Skip to content
Back to research

The AWS Passport API That Accepted Any Non-Empty Key

A practical AWS penetration-test walkthrough showing how a missing API Gateway authorizer and missing object ownership check combined into passport-record exposure, how defenders could investigate it and how the same requests behave after remediation.

YouTubeHow a penetration test stopped an AWS passport API breachOpen the related video

What happened

The whole incident in one sentence

One passport method skipped its API Gateway authorizer, the Lambda handler treated any non-empty partner key as enough, and changing a passport reference returned a different customer's record.

Five terms that make the rest easier

API Gateway is the front door that receives the web request. A method is one verb and path, such as GET /v1/passports/{passportId}. A REQUEST authorizer is a Lambda function API Gateway can call before the method to decide who the caller is. Lambda runs the application code. DynamoDB stores the passport records the code retrieves.

The complete lab chain. It starts with normal business use, stops after two fabricated records prove impact and ends by identifying the exact control that failed.
The normal control matters: Northstar retrieves its own fabricated record through the same functional application used for the security test.
Claim boundary

TravelVerify, its staff, partners and passport records are fictional. Every request and response below came from a functional private AWS lab. Two fabricated records were enough to prove the issue. We did not enumerate the dataset, and retained logs cannot prove that an unrecorded event never happened.

How the platform works, and where it was weak

A partner enters a passport reference in the TravelVerify operations portal. The browser sends that reference and an X-Partner-Key header to API Gateway. The intended design is simple: API Gateway calls the authorizer, the authorizer maps the key to one partner, the application checks that partner owns the requested record, and only then does DynamoDB return it [1] [6] [7].

The failure was a chain, not one line: the method bypassed the authorizer, the backend kept a presence-only fallback and no code compared the authenticated partner with the record owner.
The deployed method says Authorization NONE. The authorizer exists elsewhere in the API, but this route never calls it.

API Gateway skipped identity

The GET method was created with Authorization set to NONE, so API Gateway forwarded the request without a verified partner identity.

The handler confused presence with validity

The application saw a non-empty header and continued even when the registry did not recognise its digest. A string existed, but an identity did not.

The record ID became the decision

The handler retrieved the supplied passport reference without comparing the record owner with an authenticated partner. OWASP calls this Broken Object Level Authorization [6].

How we rebuilt it, stage by stage

The assessment began from the ordinary partner workflow. Each later test changed one thing that the previous response had made visible. The displayed header values are recording aliases, not reusable credentials.

Stage 1

Removing the key returned 401

What made this possibleThe normal portal request exposed the passport path, the record reference and the fact that a partner credential was sent in a header.

The baseline request returned Northstar's own record. Removing the partner header tested whether the application expected authentication at all.

Request without the partner header
GET /v1/passports/763184295 HTTP/2
Host: partners.travelverify.test

This looked encouraging, but it proved only that the header had to exist. It did not prove the value was validated.

ResultHTTP/2 401 Unauthorized

Response
The full response
{"error":"partner authentication required"}
What this proves

The same passport path that returned 200 in normal use returned 401 when the header was absent.

Control that stops it
Keep the missing-identity failure, but make API Gateway own it by attaching the authorizer to every sensitive method [1].
Lab reference
[1, 7]
Stage 2

An obviously fake key returned the passport

What made this possibleThe 401 response showed that the code distinguished a missing header from a present one, which justified testing whether it also distinguished an invalid value.

The next request supplied a value that could not be mistaken for a registered partner key. The route returned the same record and status as the valid request.

Request with an invalid value
GET /v1/passports/763184295 HTTP/2
Host: partners.travelverify.test
X-Partner-Key: definitely-not-a-real-key

Missing and invalid credentials produced different outcomes. The application was checking whether a value existed, not whether it belonged to a partner.

ResultHTTP/2 200 OK

Decisive response fields
The full response
{
  "passport_reference": "763184295",
  "partner_name": "Northstar Travel",
  "verification_status": "Verified"
}
What this proves

A visibly invalid key returned the same fabricated passport fields as the valid Northstar request.

Control that stops it
Resolve the supplied credential to one active partner inside the authorizer and refuse unknown values before the application runs [1] [7].
Lab reference
[1, 7]
Stage 3

Changing only the reference crossed the customer boundary

What made this possibleThe normal BlueJet workflow visibly supplied a second fabricated reference and confirmed that its owner was a different partner.

The separate BlueJet portal had already revealed one BlueJet passport reference. Replacing only the Northstar reference with that observed value returned BlueJet's record.

Northstar request for the observed BlueJet record
GET /v1/passports/935627140 HTTP/2
Host: partners.travelverify.test
X-Partner-Key: northstar-recording-key

A valid partner key should identify the caller, then the application should compare that identity with the record owner. The vulnerable handler did neither reliably.

ResultHTTP/2 200 OK

Decisive response fields
The full response
{
  "passport_reference": "935627140",
  "partner_name": "BlueJet Holidays",
  "verification_status": "Verified"
}
What this proves

The response named BlueJet Holidays while the request still carried the Northstar recording credential.

Control that stops it
Compare the authenticated partner ID from the authorizer context with the owner ID on every retrieved object [6].
Lab reference
[6]

What defenders would see

The difficult part is that both dangerous requests look like ordinary HTTPS GET requests at the network layer. GuardDuty analyses CloudTrail management events, VPC flow data and DNS data, but it does not know whether a custom X-Partner-Key value is valid or whether a returned passport belongs to the caller [4]. Security Hub can surface API Gateway posture controls, but its managed route-authorisation control is scoped to API Gateway V2 routes, while this lab uses a REST API method [5]. Neither service automatically reports this data exposure.

SourceWhat it can answerWhat it cannot answer by itself
API Gateway access logsWhich route returned 200, when, from which source and with which authorizer principalWhether the returned record belonged to the caller
Application audit logCredential state, authenticated partner, requested owner and allow or deny decisionWhether API Gateway was deployed with the correct authorizer
CloudTrailWho changed API Gateway, Lambda or logging configurationWhich passport fields a normal data-plane request returned
GuardDutyThreat patterns in its supported AWS, network and DNS data sourcesA custom business authorisation failure in the response path
Security Hub CSPMNarrow managed posture questions for supported API Gateway resourcesA complete REST method and backend ownership assessment
What each source can and cannot establish. The useful signal comes from logging the application decision without logging the credential value.
SQL
fields @timestamp, request_id, route, status, source_ip, authorizer_principal_id
| filter route = "/v1/passports/{passportId}"
| filter status = "200"
| filter authorizer_principal_id = "-"
| sort @timestamp asc
| limit 200
The query uses normal request fields, not a lab marker. Eight 200 responses reached the passport route without an authorizer principal in the bounded test window.
Retention limits the answer

This application had been live for one month and retained three months of logs, so its whole lifetime was available. If the issue were found a year later with the same retention, the team could examine only the latest three months. Even complete retained logs support the statement "we found no matching evidence in this window", not "this never happened" [2] [3].

How the secure design breaks the chain

The services stay the same. The method establishes identity before Lambda, and Lambda still makes a separate ownership decision before returning the record.
BeforeVulnerable method

The route invokes Lambda without a verified identity.

HCL
authorization = "NONE"
authorizer_id = null
AfterSecure method

API Gateway invokes the REQUEST authorizer first.

HCL
authorization = "CUSTOM"
authorizer_id = aws_api_gateway_authorizer.partner[0].id
The corrected delivery path runs Trivy against the Terraform and blocks the vulnerable candidate before AWS credentials or Terraform setup.
The live Secure method confirms the deployed value, rather than relying only on the Terraform source.
Secure replay

The same four requests now make four correct decisions

What made this possibleThe vulnerable responses established the exact request set that the correction had to change without breaking ordinary partner use.

The secure environment received the same missing, fake, cross-owner and valid-owner requests used against the vulnerable environment.

Replay matrix
No partner key                       -> 401
Fake partner key                     -> 403
Northstar key, BlueJet record        -> 403
Northstar key, Northstar record      -> 200

The fake key stops at the authorizer. A valid identity that asks for another partner's record stops at the backend ownership check. Normal use still succeeds.

Result401, 403, 403, 200

Decisive application reasons
The full response
missing_credential
unauthenticated
partner_does_not_own_record
record_returned
What to do about it

The paired replay changed both unsafe 200 responses to 403 while preserving the authorised Northstar 200 response.

Control that stops it
Keep an automated negative test for every sensitive method so a new route cannot silently return 200 for a missing, fake or wrong-owner identity [6] [7].
Lab reference
[1, 6, 7]

Check this on your own API

Read-only check 1

List every sensitive method and its authorisation type

What made this possibleThe live root cause was one method with a different setting from the rest of the API.

Start in API Gateway, open the REST API, choose Resources and select each method that can return or change sensitive data.

Read-only API inspection
aws apigateway get-method \
  --rest-api-id <api-id> \
  --resource-id <resource-id> \
  --http-method GET \
  --query "{authorizationType:authorizationType,authorizerId:authorizerId}"

ResultSafe answer: CUSTOM or AWS_IAM, with the intended authorizer attached

What to do about it

Any sensitive REST method that unexpectedly reports NONE recreates the first failure in this chain.

Control that stops it
Review the live method and the infrastructure definition. A source-only review misses drift; a console-only review misses what the next deployment will restore [1].
Lab reference
[1]
Read-only check 2

Trace where a credential becomes an identity

What made this possibleThe fake-key 200 proved that presence and validity were separate decisions in the vulnerable handler.

Find the code that reads the partner header and follow the invalid-value branch. A non-empty string must never create or imply a caller identity.

Source review question
registered = registry.lookup_digest(credential_digest(raw_key))
if registered is None:
    deny(403)
partner_id = registered.partner_id

ResultSafe answer: an unknown credential is denied before the data handler runs

What to do about it

The authorizer should pass a stable partner identity in context. The backend should not infer identity from the original header [1] [7].

Control that stops it
Log a safe credential reference or state such as known, unknown or missing. Never log the credential value.
Lab reference
[1, 7]
Read-only check 3

Prove ownership with negative tests

What made this possibleThe cross-partner 200 showed that successful authentication alone would not have closed the record-level failure.

Use two test tenants and two fabricated records. A valid credential for one tenant must not retrieve the other tenant's record.

Required behaviour
Tenant A credential -> Tenant A record = 200
Tenant A credential -> Tenant B record = 403
Fake credential -> either record = 403
No credential -> either record = 401

ResultSafe answer: every object access checks the authenticated tenant against the owner

What to do about it

OWASP recommends object-level authorisation in every function that uses a client-supplied object ID [6].

Control that stops it
Run this matrix in CI for every sensitive route, and alert on sequential or high-cardinality record access by one partner.
Lab reference
[6]

Reproduce the lab safely

The Practice Lab deploys isolated Range and Secure versions of the fictional TravelVerify API through GitHub Actions. It includes the functional portal, fabricated passport records, API Gateway, the authorizer and application Lambdas, DynamoDB, CloudWatch logging, the Trivy delivery gate, the exact Logs Insights queries and the paired replay. Both environments stay private and use bounded test credentials. The learner destroys them through the same workflow when recording is complete.

StageWhat the lab provesLab action IDs
Normal useNorthstar and BlueJet each retrieve their own recordN01 to N04
Authentication testMissing key returns 401 and two fake values return 200 in RangeB05 to B09
Ownership testNorthstar retrieves the observed BlueJet record in RangeO02 to O04
InvestigationLogs Insights, retention, API Gateway and Lambda explain the two unsafe 200 responsesL01 to C07
Delivery controlTrivy blocks the missing authorizer before deploymentG01 to G10
Secure replayThe same requests return 401, 403, 403 and 200F01 to F10
Reader-facing stages map to the internal lab identifiers used by the evidence package. These identifiers stay here rather than cluttering the investigation narrative.
Practice labTravelVerify broken authentication and BOLA Practice Lab

The pinned public learner release will be linked here after its separate publication review.

Practice Lab release pending (coming after release approval)
Sources and further reading

References

Numbered sources support the practical points in this article. Video timestamps will appear when the related media is published.

  1. Vendor DocAWS
    Use API Gateway Lambda authorizershttps://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html

    Explains how API Gateway calls a Lambda authorizer before invoking a REST API method and how REQUEST authorizers use headers and other identity sources.

  2. Vendor DocAWS
    Set up CloudWatch logging for REST APIs in API Gatewayhttps://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html

    Defines API Gateway access logging, execution logging and the account-level permission needed to deliver logs to CloudWatch.

  3. Vendor DocAWS
    CloudWatch Logs Insights query languagehttps://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/CWL_AnalyzeLogData_LogsInsights.html

    The query-language reference behind the investigation shown in this article.

  4. Vendor DocAWS
    GuardDuty foundational data sourceshttps://docs.aws.amazon.com/guardduty/latest/ug/guardduty_data-sources.html

    Shows why GuardDuty does not automatically understand a custom application authorisation decision or the passport fields returned in an API response.

  5. Vendor DocAWS
    Security Hub CSPM controls for Amazon API Gatewayhttps://docs.aws.amazon.com/securityhub/latest/userguide/apigateway-controls.html

    Lists the managed API Gateway posture checks and their exact resource coverage.

  6. AdvisoryOWASP
    API1:2023 Broken Object Level Authorizationhttps://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/

    Defines the record-ownership failure reproduced by changing the passport reference.

  7. AdvisoryOWASP
    API2:2023 Broken Authenticationhttps://api-security.owasp.org/editions/2023/en/0xa2-broken-authentication/

    Defines authentication failures including credentials whose authenticity is not validated.

Seen something like this in your own estate?

Find the same weaknesses before an attacker does

Talk to us