Escaping without a destination is not a defence
"Escaped" is an incomplete sentence. Escaped for what? A backslash before a quote is correct for one SQL dialect's string literal and wrong inside an HTML attribute. Replacing angle brackets with entities protects HTML element text and does nothing inside a javascript: URL. At the moment a request arrives you do not know which parser the value is destined for, and often it is destined for several, so any single transformation at the edge is correct for at most one.
That is the first half. The second half is the concept that reframes the whole topic: injection is a property of the sink, not of the input. A string is only dangerous when a parser reads it and decides that part of it is instructions rather than data. The same bytes are inert in one destination and executable in another. So the question to ask of any value is never "has this been sanitised" but "which parsers will see this, and how does each one tell code from data".
One value, several correct answers
Take the surname O'Brien and a comment containing <b>. The correct rendering differs by destination, and no edge filter can produce all of them:
Stored in the database O'Brien (unmodified; the driver handles quoting)
Rendered in HTML element text O'Brien or left alone, since a quote is inert there
Rendered in an HTML attribute O'Brien (a bare quote would close the attribute)
Inside a JSON string O'Brien with \" and \\ escaped, not entities
Passed to an external command no escaping (pass as an argv element, never a shell string)
Notice the direction of travel. Escaping is an output concern, applied by whichever component is about to hand the value to a parser, using that parser's rules. The edge's job is narrower: validate that the value is the kind of thing you expected — a length, a numeric range, a date, a member of an enumerated set — and reject it if not. That reduces the attack surface but substitutes for nothing, because plenty of legitimate values contain characters that are dangerous somewhere.
Separate code from data, rather than filtering data
The strongest defences at each sink work by never mixing the two in the first place, so there is nothing to escape.
For SQL, a bound parameter is sent to the server separately from a statement that has already been parsed. The value cannot alter the statement's structure no matter what it contains, because the parse happened before the value existed in that context. That is categorically different from escaping a value into a concatenated string, where you are relying on your rules matching the server's parsing rules exactly, across every character set and dialect quirk.
For external commands, pass an argument vector to the process rather than a command line for a shell to interpret; if no shell parses the string, there is no metacharacter to abuse. For HTML, use a template engine that escapes by default and understands context, and treat every bypass as a reviewable decision. For an interpreter you do not control — an LDAP filter, an XPath expression, a header — use the library's construction API rather than string building.
The positions a parameter cannot occupy
A bound parameter can stand where the grammar expects a value, and nowhere else. You cannot bind a table name, a column name, or the direction of an ORDER BY, because those are structural. This is where injection survives in codebases that otherwise use parameters correctly, since a sort column from a query string has to reach the SQL somehow.
The only sound answer there is an allow-list: map the client's requested sort field through a fixed dictionary of permitted values to the identifier you control, and reject anything absent from it. Do not attempt to sanitise an identifier — you are back to matching the parser's rules by hand, on the one input position where a mistake gives away the whole statement.
What escaping at the edge does to your data
Beyond not working, edge escaping causes harm, and saying so distinguishes a considered answer. Values arrive in the database pre-mangled, so the surname is stored as O'Brien and every consumer — an export, a mail merge, a partner API, a search index — either shows the entity to a human or has to guess whether to decode it. Some paths decode twice and some not at all, so the data develops inconsistent encodings nobody can clean up reliably.
It also creates the conditions for second-order injection. A stored value is later read back and concatenated into a query or a template by code that assumes anything already in the database is trusted. The dangerous flow no longer starts at a request handler, so it is invisible to a review of inbound parameters.
The sink nobody had classified
In December 2021, Log4Shell made any application that logged an attacker-controlled string a candidate for remote code execution, because Log4j performed JNDI lookups on the message it was formatting. The principle it demonstrates is that untrusted data has no inherently safe destination: a log line had not been treated as somewhere input could execute, so nobody had drawn a trust boundary there. When you reason about input validation, reason about every sink the data reaches rather than only the ones that obviously interpret it.
That is also the instruction for reviewing a real system. Enumerate the sinks rather than the sources: every place a string is handed to something that parses it, including the ones that do not look like interpreters — loggers, email templating, filenames and paths, spreadsheet cells a desktop application will evaluate as formulae, metric labels, deserialisers. For each, name the mechanism keeping data out of the instruction position.
Injection lives at the sink, so ask which parser will read the value and how that parser separates code from data. Bind parameters, pass argument vectors instead of shell strings, encode at output by context, allow-list the structural positions a parameter cannot occupy — and store user data exactly as it was given to you.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).