Skip to content
CWE-89A03:2021 – Injection

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 LIKE pattern, a LIMIT, an IN() 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

ContextQuery shapeBreak out withNotes
Single-quoted stringWHERE name = '[in]'' OR 1=1-- -The classic. Most common by far.
NumericWHERE id = [in]1 OR 1=1-- -No quote needed. Quotes will *break* it.
Double-quoted stringWHERE name = "[in]"" OR 1=1-- -MySQL in ANSI_QUOTES mode treats these as identifiers.
Parenthesised stringWHERE (name = '[in]')') OR 1=1-- -Close the paren or the query stays unbalanced.
LIKE patternWHERE name LIKE '%[in]%'%' OR 1=1-- -You also inherit % and _ as wildcards.
IN listWHERE id IN ([in])1) OR 1=1-- -Numeric, plus a paren to close.
ORDER BYORDER BY [in](CASE WHEN … THEN 1 ELSE 2 END)Identifier position — no quotes, no UNION.
LIMITLIMIT [in]1 PROCEDURE ANALYSE(…)MySQL only, and removed in 8.0. Usually a dead end.
Table/column nameSELECT [in] FROM tno quotes at allIdentifier. Parameterisation is impossible here.

Determining the Context Empirically

You rarely see the source. Infer the context from how the application reacts:

  1. Send a single quote. An error or a changed response suggests a quoted string context.
  2. 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.
  3. Send arithmetic. For a parameter with value 5, try 4+1 and 6-1. If the result matches what 5 returned, the input is being evaluated — a numeric context.
  4. 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

Payload
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

JavaScriptVulnerable
// 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}`
)

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'='1 leaves the application's own trailing quote to close your literal, keeping the statement syntactically whole without a comment at all.