A "forgot password?" page. You type an email, and the page answers only yes or no: whether an account with that email exists. No data and no error messages are shown. But the email goes into an SQL query. The accounts table has a reset_token column for the admin, and that is what you want to extract one character at a time.
The Ravens accounts app has two pages. The reset page takes an email and answers only yes or no — whether an account exists — showing no data, no errors and no query. But the email goes into an SQL query. A second page submits the token you recover. The accounts table has a reset_token column for the admin: that is your target.
Extract the admin's full reset token, then enter it.
You enter the admin's correct reset token.
Why the solution worked
Even without the server showing any data, its answer changing (yes/no) based on a condition you control leaks the information one bit per request. With a sequence of yes/no questions you extract a whole value. Hiding errors and result messages does not close the hole; the problem is that the input still becomes a condition in the query. The fix is the same: parameters that separate the value from the query text.
The root cause
Hiding output is not protection while the input controls the query logic. The yes/no answer changing with a condition you control leaks one bit per request. The root is the same: input pasted into the query text.
How the code is fixed
Blind injection shows that hiding output is not protection while the input still changes the query logic. Parameters close the hole. It is also better not to reveal whether an account exists at all.
Vulnerable code
app.post('/reset', (req, res) => {
const email = req.body.email;
// Only a yes/no is returned, but the input still shapes the query's logic.
const sql = "SELECT 1 FROM accounts WHERE email = '" + email + "'";
res.json({ exists: db.get(sql) !== undefined });
});
Safe code
app.post('/reset', (req, res) => {
// The email is data: it can only ever be compared as a value, never run as logic.
const exists = db.get('SELECT 1 FROM accounts WHERE email = ?', [req.body.email]) !== undefined;
// And do not reveal whether an account exists: answer the same either way.
res.json({ message: 'If that email has an account, a reset link has been sent.' });
});
Impact in the real world
Blind injection is slower than the visible kind but still extracts whole values — reset tokens, password hashes, any column — and automation tools make it very fast in practice.
How defenders detect it
Defenders watch for many near-identical requests from one source in a short time, and for substr or and 1=1 in inputs, and rate-limit; uniform responses also stop the page revealing whether an account exists.