Vulnerability
[PASS] Broken Object Level Authorization - GraphQL - Transaction Summary
Description
The test attempted to read another user's transaction summary through the accountNumber argument of the GraphQL transactionSummary resolver, including the victim's account and the administrator's account.
Impact
No impact. Control holds: the resolver compares the requested accountNumber with the account owned by the token holder and rejects cross-object requests with an explicit error. Ordinary users can only read the transaction summary of their own account.
Note: the same code path is still flawed for callers with the administrator role, because the argument value is passed into the SQL query without parameterization; this is reported separately in the SQL Injection - GraphQL finding.
CWE
CWE-639: Authorization Bypass Through User-Controlled Key
OWASP
A01:2021 - Broken Access Control
Scenario
Normal: an ordinary user requests their own account transaction summary -> succeeds. Attempt: accountNumber is changed to the victim's account -> rejected. Attempt: accountNumber is changed to the administrator's account -> rejected.
Endpoint
POST http://127.0.0.1:5001/graphql
Affected feature
GraphQL transactionSummary (object authorization)
Remediation
- Keep the account ownership check in the resolver.
- Add parameterized queries so the same argument does not become an injection point for higher-privileged callers.
Proof of concept
- (Screenshot 1) NORMAL: an ordinary user requests their own account transaction summary -> succeeds.
- (Screenshot 2) ATTEMPT: accountNumber is changed to the victim's account -> rejected with the error You can only query your own transaction summary, control holds.
- (Screenshot 3) ATTEMPT: accountNumber is changed to the administrator's account -> rejected, control holds.
The complete record
LAB All targets are lab systems. Nothing here is a client engagement. Evidence is shown as recorded; secrets are blacked out.
HTTP request
POST /graphql HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
Content-Type: application/json
Content-Length: 148
{"query": "query($a:String){transactionSummary(accountNumber:$a,limit:3){scope accountNumber totalTransactions}}", "variables": {"a": "0932191785"}}
HTTP response
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 149
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:13:13 GMT
{
"data": {
"transactionSummary": {
"accountNumber": "0932191785",
"scope": "account",
"totalTransactions": 11
}
}
}
HTTP request
POST /graphql HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
Content-Type: application/json
Content-Length: 148
{"query": "query($a:String){transactionSummary(accountNumber:$a,limit:3){scope accountNumber totalTransactions}}", "variables": {"a": "9159912486"}}
HTTP response
HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
Content-Length: 361
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:13:14 GMT
{
"data": {
"transactionSummary": null
},
"errors": [
{
"message": "You can only query your own transaction summary.\n\nGraphQL request:1:18\n1 | query($a:String){transactionSummary(accountNumber:$a,limit:3){scope accountNumber totalTransactions}}\n | ^",
"path": [
"transactionSummary"
]
}
]
}
HTTP request
POST /graphql HTTP/1.1
User-Agent: python-requests/2.33.1
Accept-Encoding: gzip, deflate, br, zstd
Accept: */*
Connection: keep-alive
Authorization: Bearer [REDACTED-LAB-TOKEN]
Content-Type: application/json
Content-Length: 146
{"query": "query($a:String){transactionSummary(accountNumber:$a,limit:3){scope accountNumber totalTransactions}}", "variables": {"a": "ADMIN001"}}
HTTP response
HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
Content-Length: 361
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:13:15 GMT
{
"data": {
"transactionSummary": null
},
"errors": [
{
"message": "You can only query your own transaction summary.\n\nGraphQL request:1:18\n1 | query($a:String){transactionSummary(accountNumber:$a,limit:3){scope accountNumber totalTransactions}}\n | ^",
"path": [
"transactionSummary"
]
}
]
}
← Previous case: Broken Object Level Authorization - Accounts - Balance Lookup Next case: Broken Object Level Authorization - Merchant Payments - Payment By ID →