04. Mistakes

Injection: a syntax decision with a thirty-year tail

Joining text destroys the distinction between what the programmer wrote and what arrived from outside.

Teams applying this historical lesson to current delivery work can also compare tools for employee monitoring, keeping operational records separate from the code and its documentation.

injection.src
null.java if (ref == null) throw ...
// a billion dollars, by his own estimate

The mechanism, stated generally

A program builds a command by joining fixed text with text that came from outside. The result is handed to something that parses it.

At that point the parser has no way to know which characters the programmer wrote and which arrived from elsewhere, because the distinction was destroyed by the joining. Anything the supplied text contains that the parser treats as syntax is syntax.

Where it has been seen before in this document

This is in-band signalling, and it is the third appearance of the same idea. A string terminated by a value that could also be data. Line breaks that are characters in the text rather than separators between records. Now instructions and arguments in one channel.

The general failure is putting control information and content in the same stream and hoping to tell them apart afterwards. The general repair is a second channel.

1998The first public description

The technique was described publicly in an underground technical magazine in the late nineties, applied to database queries assembled by web applications.

The alternative was available at the time and had been for years: send the command with placeholders and the values separately, so the values never pass through the parser.

Why it persists

Because joining text is the shortest thing to write in every language, and the safe form is longer, less readable in simple cases, and occasionally does not support the shape of query somebody wants.

Twenty-five years after the first description it remains near the top of every list of common web application defects, which is not a statement about ignorance. It is a statement about what the default path in a language is, and about how much of software is written under time pressure by people who will not read the list.

Escaping is not the repair

The intuitive fix is to inspect the supplied text and neutralise anything dangerous. This fails repeatedly and for structural reasons: the set of dangerous sequences depends on the parser, the encoding, the context within the command, and the version.

Every filter is a claim to have enumerated a set that the other side is free to extend. Separation makes the question unaskable, which is why it works and filtering does not.

The same failure everywhere else

Commands assembled for a shell. Paths assembled for a file system. Queries assembled for a directory service or a document store. Text assembled into a template that will be rendered as code. Log lines assembled from user input and later parsed by something else.

The family is large and it is one family, which is worth saying because each member is usually taught as its own topic with its own list of dangerous characters.

2020sThe newest member

Systems built around language models assemble instructions and supplied content into one stream of text and hand it to something that interprets the whole thing. Content that resembles instruction is treated as instruction.

It is the identical failure, and the identical repair is not available, because these systems have no separate channel: the interpreter's entire input is one undifferentiated sequence by construction. Which is why the mitigations offered are filters, and why this entry's argument about filters applies to them.

The one case where filtering is the only option

Where the receiving parser is fixed and offers no second channel, separation is unavailable and something must inspect the text. That is the position rendering markup into a page has always been in, and it is why that problem has produced two decades of libraries rather than a fix.

The reasonable practice there is to parse into a structure and re-serialise it rather than to search for dangerous sequences, which is separation reconstructed at the cost of doing the parser's job twice.

2020sWhy the newest case is harder than it looks

The obvious proposal is to mark which parts of the input are instructions and which are content. That requires the interpreter to honour the marking, and these interpreters have no mechanism that forces them to; the marking is itself just more text.

Which means the property being asked for does not exist in the system as built, and claims that a particular arrangement of markers solves it should be read as claims about how the system usually behaves rather than about what it guarantees.

One channel: the data becomes instructionselect * where name = ’whatever they sentthe parser cannot tell which characters you meant and which they didTwo channels: it cannotselect * where name = ?the value, separatelynever parsedescaping filters the channel. separation removes the question.
FigureA command assembled by joining text, in which supplied data becomes instruction, against a form where the value travels separately and is never parsed.

What to take from the family resemblance

When a new interface appears, the useful question is whether control and content travel together. If they do, the failure is available whether or not anybody has demonstrated it yet, and the remedies will be filters until somebody designs a second channel.

That question is answerable from a specification, before any code exists, which makes it one of the cheapest checks in this document.

What we cannot verify

The original publication and the defect rankings are public. The rankings are compiled by an industry body from contributed data and reflect what is reported rather than what occurs. Claims about the prevalence of any particular defect class should be read as statements about reporting.

In short

  1. Joining text destroys the distinction between what you wrote and what arrived.
  2. It is in-band signalling, and the third appearance of that idea in this document.
  3. The safe form existed before the attack was described publicly.
  4. It persists because joining is the shortest thing to write in every language.
  5. Escaping enumerates a set the other side may extend; separation removes the question.
  6. Systems built on language models are the newest member, without a second channel.

also in Mistakes

Next.

further context

For a primary or institutional reference, see the Unicode Standard.

Every claim here carries the source it came from.

The source and its year sit beside the sentence they support. A secondary account is marked as one, and where the record is unclear the entry says so rather than choosing the better story.