Injection attacks
Injection is one bug with many faces — SQL, OS command, NoSQL, LDAP — and all of them are the same mistake, attacker data crossing into an interpreter as code. See how an attacker reasons about it, and why parameterization is the only fix that closes the class.
On an authorized engagement, a tester types a single apostrophe into a product-search box and submits. The page returns a 500 with a fragment of a database driver error. To the developer that error is a nuisance to suppress. To the tester it is a confession: the apostrophe they typed reached the database and changed the structure of a query — which means the box isn’t a search field at all, it’s a programming interface into the backend. They didn’t break in with a tool or a CVE. They typed one character that the application’s own code obediently pasted into a SQL string and handed to the engine. Everything that follows — reading other users’ rows, dumping the password-hash column, sometimes running OS commands on the host — is just elaboration on that first fact: the application could not tell their data apart from its code. This is injection, the oldest and most durable web-app vulnerability class there is, and it is one mistake wearing a dozen different costumes.
By the end of this lesson you’ll be able to explain how an attacker recognizes and reasons about an injection point, and why parameterization — not escaping or filtering — is the control that actually closes the whole class.
Authorization first: this is a defender’s autopsy
Everything below assumes a signed engagement with explicit scope — a deliberately vulnerable lab, a CTF, your own app, or a bug-bounty program with the target in its asset list. Probing a live application you don’t own for injection is unauthorized access in most jurisdictions, full stop; a single malformed query against someone else’s database can corrupt or destroy data, so “I was just testing” is not a defense. We study the attacker’s reasoning for one reason: you cannot reliably close a vulnerability class you only understand as a checklist item. Read this as the autopsy a defender performs on their own code, learning to see the query the way the attacker does so they can break the attack before it exists. No payloads to copy live here — only the mechanism, so you can recognize it in a code review and kill it in design.
The one mechanism behind every “injection”
SQL injection, OS command injection, NoSQL injection, LDAP injection, XPath injection, even server-side template injection — security people name them separately, but they are the same bug in different interpreters. The mechanism is always this: the application builds an instruction (a SQL statement, a shell command, an LDAP filter) by concatenating trusted code with untrusted input as text, then hands the combined string to an interpreter that re-parses the whole thing. The interpreter has no idea which bytes the developer wrote and which the user supplied — it only sees one string, and it parses it by its own grammar. If the user’s bytes contain characters that are syntactically meaningful in that grammar — a quote that ends a string literal, a semicolon that ends a statement, a backtick that opens a subshell — the user has just written code, and the interpreter runs it.
That reframing is the whole lesson. The vulnerability is not “the user typed something bad.” It is that the system mixed two trust levels into one channel and then parsed that channel with a grammar. The attacker’s entire job is to find a place where their input lands inside an interpreted string, then supply input that breaks out of the data slot and into the code slot. The apostrophe in the search box is exactly that: in SQL, ' terminates a string literal, so an input that contains one can close the literal the developer intended and continue with attacker-chosen SQL.
How an attacker reasons about an injection point
An attacker doesn’t start by knowing the bug is there — they probe for the boundary between data and code. The reasoning is a small, disciplined loop, and understanding it is what lets a defender reproduce and fix it.
- Find the sink. Locate where user input plausibly reaches an interpreter — a search filter, a sort parameter, an
idin a URL, a login form, an export filename, a header. Anything the app might feed into a query or command. - Perturb the syntax. Send input containing a character that is special to the suspected grammar (for SQL, a quote; for a shell, a metacharacter) and watch what changes. A 500 error, a different result count, or a timing change are all signals that the input reached a parser and altered it.
- Confirm and characterize. Distinguish a real injection from a coincidental error by toggling between input that should keep the syntax valid and input that should break it. If “valid” loads normally and “broken” errors, the input is being parsed — that’s a confirmed injection point.
- Determine the context and channel. Figure out which interpreter (SQL? shell? LDAP?) and whether results come back in the response (in-band) or must be inferred (blind).
The contexts: same bug, different interpreter and blast radius
The reason injection stays at the top of every risk list is that it generalizes across every interpreter a backend talks to — and the blast radius scales with what that interpreter can do.
| Context | Interpreter / sink | What the attacker gains | The structural fix |
|---|---|---|---|
| SQL injection | SQL engine via a string-built query | Read/modify any row; dump password hashes; sometimes file/OS access | Parameterized queries (bound values) |
| OS command injection | A shell invoked with a built-up command string | Run commands as the app user — full host compromise potential | No shell: exec the binary with an argument array |
| NoSQL injection | Document-DB query built from request objects | Auth bypass via operator injection; data exfiltration | Typed query builders; reject operator-shaped input |
| LDAP / XPath | Directory or XML filter built by concatenation | Auth bypass; enumerate directory entries | Parameterized/escaped filter APIs |
OS command injection deserves a special note because its blast radius is the worst on the list. A SQL injection compromises a database; a command injection compromises the host, running with the privileges of the application process — which on a poorly-isolated box can mean the whole machine and everything it can reach. The root cause is the same string-concatenation mistake, just with a shell as the interpreter instead of a database. And the structural fix mirrors the SQL one: don’t hand a string to a shell that re-parses it for metacharacters — invoke the target program directly with its arguments passed as a discrete list, so the OS never runs a shell that could interpret a semicolon or a pipe as a new command.
Blind injection: when the answer doesn’t come back
Beginners assume injection requires the database to echo data into the page. It doesn’t. Blind injection is the case where the application doesn’t return query results or errors to the attacker — but the attacker can still extract data one bit at a time by asking yes/no questions and observing a side effect. The two classic channels:
- Boolean-based: craft input so the query is true in one case and false in another, and watch the response differ — a different page, a different length, a present-or-absent element. Each request leaks one bit (“is the first character of the admin hash an ‘a’?”).
- Time-based: make the query sleep when a condition is true. The data never appears anywhere; the attacker reads it purely from how long the response takes. This is slow — often automated, exfiltrating a hash character by character over thousands of requests — but it is complete: a blind, time-based channel can extract an entire database given enough patience.
The defensive lesson from blind injection is brutal and clarifying: suppressing error messages does not fix injection. Teams routinely “remediate” by hiding the database error that first revealed the bug. The injection point is still there; the attacker just switches to a blind technique and keeps going, now without the convenient error to tip off your monitoring. Hiding the symptom removes your own signal and leaves the vulnerability fully exploitable.
▸Why this works
Why doesn’t input filtering or escaping reliably close injection, when parameterization does? Filtering (“strip quotes and semicolons”) is a blocklist, and blocklists lose: every interpreter has more dangerous syntax than you remembered (comment sequences, encodings, Unicode look-alikes, alternate operators), and one missed case re-opens the hole. Manual escaping can be correct, but it puts the burden on every developer to escape perfectly for the exact context every single time, and the one query someone forgets is the breach. Parameterization wins because it changes the architecture, not the input: the query structure is sent to the engine first and compiled, then the values are bound to placeholders through a separate channel that is never parsed as syntax. There is no string for the attacker to break out of — the data slot and the code slot are physically different channels. It is safe by construction rather than safe by vigilance, which is why it scales to a whole codebase and a blocklist never does.
Why parameterization is the only fix that closes the class
Tie it together. Injection exists because code and data share one channel that an interpreter re-parses. Every durable fix works by separating those channels at the architecture level so untrusted input is delivered as a value that is structurally incapable of being interpreted as syntax. For SQL, that’s parameterized queries (a.k.a. prepared statements / bound parameters): the database receives the query template, plans it, and only then accepts the values — which it treats as opaque data, never as part of the statement’s structure. For OS commands, it’s invoking the binary with an argument vector instead of a shell string. For NoSQL, it’s typed query builders that reject operator-shaped input. Defense-in-depth still matters — least-privilege database accounts so a successful injection reads less, input validation as an allowlist to reject malformed input early, and structured logging to detect probing — but those reduce the damage; they don’t close the hole. Only separating code from data does that, and it’s why the parameterized query, not the WAF rule, is the senior’s first and last answer to injection.
A search endpoint builds its SQL by concatenating the user's query string into a WHERE clause, and a tester confirmed SQL injection. Pick the fix that actually closes the class.
What is the single root cause shared by SQL injection, OS command injection, and LDAP injection?
A team 'fixes' a SQL injection by hiding the database error message that exposed it. Why is the application still exploitable?
Order how an authorized tester reasons from a suspicious input field to a confirmed injection, then to the durable fix:
- 1 Find a sink where user input plausibly reaches an interpreter (search, id, login)
- 2 Perturb the syntax with a character special to the suspected grammar and watch for a change
- 3 Confirm by toggling syntactically-valid vs broken input and characterize the context/channel
- 4 Close the class structurally: parameterize the query so input is bound as data, not parsed as code
- 01Explain the single mechanism behind every injection class and how an attacker reasons their way to a confirmed injection point.
- 02Why does parameterization close injection when filtering, escaping, and WAF rules don't — and what about blind injection makes hiding errors useless?
Injection is one vulnerability wearing many costumes — SQL, OS command, NoSQL, LDAP, XPath, template — and every one is the same mistake: the application concatenates untrusted input with trusted code into a single string, then hands it to an interpreter that re-parses data and code together by its own grammar. When the input contains characters that are syntactically meaningful (a quote, a semicolon, a shell metacharacter), it stops being data and becomes code the interpreter runs. An authorized tester finds these points with a disciplined loop — locate a sink, perturb the syntax, confirm by toggling valid vs broken input, characterize the context and channel — and the blast radius scales with the interpreter: a SQL injection compromises a database, an OS command injection compromises the host. Blind injection (boolean- and time-based) shows why surface fixes fail: with no result and no error returned, an attacker still extracts data one bit at a time from response differences or timing, so filtering characters, hiding errors, and bolting on a WAF rule all leave the hole open while removing your own signal. The only fix that closes the class is structural — separate code from data so untrusted input is delivered as a value that is incapable of being parsed as syntax: parameterized queries, argument-array exec, typed query builders. So the next time you read a handler that builds a query by gluing a string together, your reflex is the attacker’s question turned inside out — can any byte of this input change the structure of what runs, or only its values?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.