The shop has a little stock-check API. Give it a sweet id and it tells you whether that sweet is in stock. No data, no error messages, just a yes or a no. The id goes straight into a SQL query, so it is injectable, but there is nothing to read in the response. That is fine. A single yes-or-no is all you need to read the whole database, one character at a time. This is blind SQL injection.
New to this? What is blind SQL injection?
Blind SQL injection is SQL injection where the app never shows you the query results or the error, so you cannot just read data out. Instead you ask the database true or false questions and watch how the answer changes.
“Is the first letter of the admin’s password an A?” If in-stock flips, the answer is yes. Ask enough of these and you spell out any value, slowly but surely. Tools like sqlmap automate the whole thing.
Make sure the shop is running at http://localhost:3000.
No account needed.
1 · Find it
This is an API, so the response is a small blob of JSON text rather than a designed page. Type this into your browser’s address bar and press Enter:
http://localhost:3000/api/stock?id=1 The page shows {"in_stock":true}. Now bolt a condition onto the id and watch the
answer move (the browser turns the spaces into %20 for you, that is fine):
http://localhost:3000/api/stock?id=1 AND 1=1 http://localhost:3000/api/stock?id=1 AND 1=2 The first stays true, the second flips to false. Your input is
changing the query’s logic, so it is injectable, and the in-stock flag is your one-bit
window into the database.
2 · Exploit it
Easy · read one character by hand. Turn that yes/no into data. Paste this into the address bar: it asks whether the first character of the admin’s password hash is a nine.
http://localhost:3000/api/stock?id=0 OR (SELECT substr(password_hash,1,1) FROM customers WHERE email='owner@example.com')='9' It comes back {"in_stock":true}, so it is. Change the 1 in
substr(...,1,1) to 2 to check the second character, and change the
'9' to test other values (0-9, a-f for a hex hash). One
character at a time, by hand, you can spell out the whole hash, then crack it.
Hard mode · let sqlmap grind. Doing that by hand is slow, so in practice a tool automates every question. Point sqlmap at the endpoint and it walks the database for you:
sqlmap -u "http://localhost:3000/api/stock?id=1" -p id --dump Why it matters: “we do not show query results” is not a defence. One observable difference, a flag, a timing delay, an error or not, is enough to extract everything.
3 · Understand it
The id is concatenated into the query, exactly like the search box, but here only a count comes back as a boolean:
const sql = "SELECT COUNT(*) AS n FROM sweets " +
"WHERE id = " + id + " AND price_pence > 0";
const row = db.prepare(sql).get();
res.json({ in_stock: row.n > 0 }); 4 · Fix it
Same fix as any injection: parameterise, so the id is bound as a value and can never change the query’s shape:
"... WHERE id = " + id db.prepare(
'... WHERE id = ? ...'
).get(id); Validate that the id is an integer while you are there. A blind hole leaks just as much as a loud one, only slower.
The general rule: parameterise every query, on every endpoint, even the ones that only ever answer yes or no.