Security Engineering · Web Application Security

The Places Parameter Binding Cannot Reach

Identifiers, sort direction, operators, and collation are resolved before execution, so binding cannot protect them; the designs that survive are structural.

Parameter binding fixes the statement’s syntax before any value exists, so a value can never become syntax. That is a complete solution to one problem and no solution at all to a neighbouring one: some parts of a statement are syntax by construction, and an application that lets a user steer them is choosing that syntax on the user’s behalf. Applying the same defensive posture there requires a different mechanism, because there is nothing to bind.

The line between the two is the parse. Anything the parser must see to build the plan is syntax; anything that only supplies a datum to the plan is a value. WHERE id = ? is a value position, because id is fixed by the parse and the parameter supplies a comparison operand. ORDER BY ? is not expressible, because the grammar needs a sort key at parse time. Every dynamic-query vulnerability of this class lives in the gap between what the developer thinks of as data and what the parser treats as structure.

Identifiers selected by name

A table or column name chosen from a request parameter is the common case. A report builder lets the user pick which columns to display; an admin panel lets them sort by a field; a search API lets them filter on one of several attributes. The natural implementation assembles the name into the statement.

The safe implementation does not sanitise the name; it looks it up. The request carries a key, the application holds a mapping from keys to identifiers, and only the mapped identifier’s text is assembled. The property this provides is not “the name is clean” but “the name is one the developer wrote”. An attacker who supplies an arbitrary string gets a lookup miss, which is a different outcome from a string that was cleaned and still diverges from what the parser sees.

Keying on the mapped identifier rather than the input matters even when the input passes a character check. Database identifiers can be quoted with double quotes, and a quoted identifier can contain almost any character, including a double quote if doubled. An allowlist based on a regular expression for ordinary identifier syntax therefore either rejects legitimate quoted names or admits sequences that the quoting layer will interpret differently from the validator. A lookup table has neither problem, because it never admits a name the developer did not write down.

The lookup also handles schema and catalog qualification. A mapping that resolves email to public.users.email fixes the schema at the same time, which matters in deployments where search_path is influenced by anything else in the session. If the resolution has to be dynamic — a multi-tenant schema per customer, for example — the dynamic part is the schema name, and it should be selected the same way, from a mapping whose keys are tenant identifiers the application itself issued.

Structure chosen by the client

A more subtle case is the query whose shape depends on input. A filter form with several optional criteria, a sort with a direction, a pagination mode that chooses between keyset and offset — each produces a different statement, and each is a place where a request influences syntax.

The pattern that keeps the property is a fixed set of shapes with values bound inside each. The application decides which shape from a small enumerated input, then renders that shape’s statement text with parameters for every value. The enumeration is the security control: the input selects among statements the developer wrote, and cannot produce a statement the developer did not write.

Where the combination of optional criteria is large, the shapes can be composed from fixed fragments rather than written out one by one — a clause per criterion, each a constant string, joined by AND when the criterion is present. The composition is safe because the fragments are constants and the joining is unconditional. Nothing in the composition is derived from the request except the decision of which fragments to include, and that decision is a boolean per fragment, not a string.

The failure mode of this pattern is fragment selection driven by a field name. An implementation that keeps a dictionary of clauses keyed by the column name and then interpolates the key rather than looking up the clause has reintroduced the problem in a form that looks like the safe version. The distinction is whether the request-supplied string ever reaches the statement, even as a key that produced a match. Looking up a request key in a dictionary and using the dictionary’s value is safe; using the key after confirming it exists is not, and the two are one character apart in the source.

Sort direction and the limits of enumeration

Sort direction is the smallest instance and the one most often handled badly. ASC and DESC are keywords, not values, so a direction parameter has to be mapped. A check for desc followed by appending a constant is safe; a check that lowercases the input and appends it is safe only if the check is an equality test rather than a prefix or a substring test. Input of desc, (select ...) passes a substring test and becomes a syntax tree the developer did not write.

Some engines accept a parameter in the LIMIT position and some do not, and the difference is grammatical rather than a feature toggle. Where the engine requires a literal, the correct handling is to convert the value to an integer in application code and interpolate the integer’s decimal representation. The conversion is a parse into a type whose serialisation has no syntax, which is the general principle: if the application has to interpolate, interpolate a value whose serialisation is guaranteed to be inert.

Operators and comparison semantics

An operator chosen from input is syntax. A search filter that allows equals, contains, or starts with maps onto different predicates — =, LIKE '%' || ? || '%', and LIKE ? || '%' — and the mapping from the request’s key to the operator is a constant. The operands remain bound, so the pattern is expressible without interpolating user text.

Collation is the case most easily overlooked. A sort that accepts a collation name, a locale, or a null-ordering preference is accepting identifiers that the parser resolves to functions or to an ORDER BY modifier. Engines that support NULLS FIRST and COLLATE accept them only as syntax, and a request that reaches that position is a syntax injection even when one writer meant it to be a display option. The mapping approach applies: enumerated inputs, constant strings.

The JSON path is a further case, because JSON path expressions are themselves a small language. An engine that accepts a parameter inside a path operator is binding the path string, which is fine, but the path string is then interpreted by the JSON path parser, so an attacker who controls the path controls a nested grammar. This is not an SQL injection; it is the same pattern at one level down, and the handling is the same: restrict the path to a fixed set or to a validated subset produced by the application rather than by the client. The template injection analysis treats nested evaluators at greater length.

When the statement must be built at runtime

Stored procedures and reporting tools sometimes have a genuine need to construct a statement from a variable. The database-side facilities for this — EXECUTE on a constructed string in PostgreSQL’s procedural language, EXECUTE IMMEDIATE in Oracle’s, dynamic SQL in SQL Server’s — take a statement string and, in some engines, a list of parameters to bind into it. The safe form passes parameters through that list, so the constructed statement contains placeholders rather than values. The constructed text still contains whatever identifiers were concatenated into it, so the same mapping discipline applies to the construction.

For the identifier part, engines offer a quoting facility: a function that takes an identifier string and returns it quoted according to the engine’s rules. This is safe for a single identifier that the application has already validated, and it is not a substitute for validation, because quoting protects against syntax breaks and not against selecting a table the user was not meant to read — pg_catalog and information_schema are valid identifiers and quoting them changes nothing. The pair is: quote to preserve syntax, allowlist to control reach. Either alone is incomplete.

The test that finds the gap

A review of a dynamic query can be reduced to a single question per interpolation site: is the variable at this position resolved by the parser before or after parameter binding? Values are after; identifiers, keywords, operators, and clauses are before. The before-sites are exactly the ones a parameter cannot occupy, and each needs a mapping whose domain is a set the developer wrote.

The complementary check is a search for the operations that indicate interpolation: string concatenation or formatting in the same expression as a statement, and template literals containing SQL keywords. Each hit is either a value that should have been bound or a structural decision that should have been mapped, and the two are distinguished by what the interpolated variable would have been in the parse — not by what it looks like.

The reason this matters more than it seems is that the injection class is usually reported as solved once binding is adopted, and the residual sites are then invisible to the review that confirmed the adoption. A codebase with complete binding and one identifier-mapping shortcut is a codebase with one dynamic query, and that query is the one an attacker will find.

The general form

The same shape recurs outside SQL. A file path assembled from a request parameter has a directory component that the filesystem resolves, and the resolution is the grammar; an allowlist of keys mapped to paths is the analogue of the identifier table, as is the containment check that verifies the resolved path remains under the intended root after normalisation. A shell command with a user-selected subcommand is a structure decision; an argument that is a value goes through the argument vector. The invariant is stable across all of them: bind what the parser has not yet seen, and map what it has.

References

  1. PostgreSQL Documentation — PREPARE — PostgreSQL Global Development Group
  2. RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format — IETF
  3. ISO/IEC 9075: Information technology — Database languages — SQL — ISO/IEC
  4. OWASP Query Parameterization Cheat Sheet — OWASP Foundation