Injection is one of the oldest application security problems, but it is not simply an old SQL mistake that modern frameworks made disappear.
The deeper problem is still relevant wherever an application takes data from an untrusted source and places it into a language, query, command or expression that another component will interpret. If the application fails to preserve the boundary between data and instructions, an attacker may be able to change what that interpreter does.
OWASP Top 10:2025 lists Injection as A05 and describes SQL, NoSQL, operating system command, ORM, LDAP and expression-language injection among the common forms. The interpreters differ, but the security question is remarkably consistent: can external input alter the structure or meaning of an instruction that the application intended to control?
That question is more useful than memorising a collection of dangerous characters.
What is an injection attack?
An injection vulnerability exists when externally influenced input reaches an interpreter in a form that lets the input affect instruction syntax rather than remain ordinary data.
The interpreter might be a SQL database, an operating system shell, a NoSQL query engine, an LDAP directory, a template engine, a browser or another component with its own language and control characters.
Imagine an application that intends to express this idea:
Find the customer whose account ID equals the value supplied by the user.
If the application sends a fixed query plus a separately bound value, the database can treat the user’s value as data. If instead the application builds one SQL string by concatenating that value into the query, the database receives a single piece of text containing both application-controlled syntax and attacker-influenced text. That is the dangerous boundary.
This is why OWASP’s main prevention principle is to keep data separate from commands and queries.
The same weakness appears in different interpreters
Injection is a family of weaknesses rather than one exact exploit technique.
| Injection type | Interpreter | What unsafe construction can change | Preferred design direction |
|---|---|---|---|
| SQL injection | Relational database | Query logic, selected rows or database operations | Parameterized queries or prepared statements |
| OS command injection | Shell or command processor | Commands, separators, options or arguments | Avoid OS commands, otherwise use fixed executables and separated arguments |
| NoSQL injection | NoSQL query engine | Query operators, filters or document-selection logic | Typed query APIs, schema validation and safe query construction |
| LDAP injection | LDAP directory | Search filter or directory operation logic | Safe LDAP APIs plus context-correct escaping where required |
| Expression or template injection | Expression or template engine | Evaluated expressions, object access or execution paths | Do not evaluate untrusted expressions, restrict capabilities and use safe templating modes |
| Cross-site scripting | Browser | HTML or script context interpreted by the browser | Context-aware output encoding and safe DOM APIs |
The table is deliberately high level. The correct defense depends on the target interpreter. A SQL parameter is not an HTML encoder, and escaping HTML does not make an operating system command safe.
SQL injection: when data becomes query structure
MITRE defines CWE-89 as improper neutralization of special elements used in an SQL command. The classic implementation mistake is dynamically building SQL with externally influenced strings.
A vulnerable pattern might look like this:
query = "SELECT id, email FROM users WHERE email = '" + email + "'"
The problem is not merely that a quote character might appear in email. The problem is architectural: application-controlled SQL syntax and untrusted data are being assembled into the same command string.
A safer pattern is to let the database driver bind the value separately:
cursor.execute(
"SELECT id, email FROM users WHERE email = ?",
(email,),
)
Here the SQL statement has a fixed structure and the driver receives email as a value. The exact placeholder syntax varies by database and driver, but the principle is the same.
OWASP’s SQL Injection Prevention Cheat Sheet recommends prepared statements with parameterized queries as a primary defense. Stored procedures can also be safe when they are properly parameterized, but a stored procedure that constructs dynamic SQL internally can reintroduce the same weakness.
Parameterization does not solve every dynamic SQL problem
A subtle mistake is assuming that every piece of SQL can be bound as a value. Parameters normally represent data values, not structural elements such as table names, column names or sort keywords.
If a reporting screen lets a user choose a sort field, do not insert an arbitrary request value into ORDER BY. Map a small set of allowed application choices to known identifiers instead:
allowed_sort = {
"name": "display_name",
"created": "created_at",
}
sort_column = allowed_sort.get(requested_sort, "created_at")
query = f"SELECT id, display_name FROM users ORDER BY {sort_column}"
The user controls only the selection key. The actual SQL identifier comes from a fixed server-side mapping.
That illustrates an important distinction: parameterization protects values, while allowlisting is often needed when the application genuinely has to select from a limited set of query structures.
Command injection: the shell is an interpreter too
OS command injection occurs when externally influenced data is used to construct a command in a way that can change the intended command or its arguments. MITRE tracks this as CWE-78.
The strongest defense is often not better escaping. It is to avoid invoking an operating system command at all.
For example, if an application needs to create a directory, use the language or platform filesystem API instead of building a mkdir shell command. A library call usually has a narrower purpose and does not expose an entire command language.
When process execution is genuinely required, prefer APIs that keep the executable and its arguments separate and avoid a shell unless the shell itself is necessary:
import subprocess
subprocess.run(
["/usr/bin/file", "--", uploaded_path],
check=True,
shell=False,
)
This example assumes uploaded_path has already been validated as an application-owned path. Passing an argument list with shell=False avoids shell interpretation of characters such as semicolons or command substitutions.
But there is an important limitation: avoiding a shell does not automatically make every argument harmless. Some programs interpret attacker-controlled values as options or have dangerous features of their own. OWASP’s OS Command Injection Defense Cheat Sheet calls this related problem argument injection.
If untrusted input must become a command argument, constrain it to the exact values or format the application needs. Use a fixed executable path, an allowlist where practical, least privilege, and a process API that does not invoke a shell.
NoSQL injection is not “SQL injection without SQL”
NoSQL databases often use objects, JSON-like structures or query operators instead of SQL strings. That changes the syntax, but not the trust-boundary problem.
Suppose an API expects a username string but accepts an arbitrary JSON object and passes that object directly into a database query. If the database driver treats special object keys as operators, the client may be able to influence query logic rather than provide only the expected username value.
The defense is not to stringify everything. Define the expected schema and types at the application boundary. If username must be a string, reject arrays, objects and operator-like structures rather than passing them through to the query layer. Then build the database operation through the driver’s intended typed query interface.
This is a good example of why input validation is valuable even when a safe API is already used. Validation expresses what the application considers legitimate data. It should support the primary structural defense, not replace it.
LDAP, expression and template injection follow the same trust rule
LDAP filters have their own special characters and escaping rules. Expression languages and template engines may allow access to variables, methods, object graphs or executable features. If untrusted text is inserted into those languages and then evaluated, the application’s meaning can be changed.
The safest design is usually to avoid treating user-controlled text as executable language in the first place.
If a feature genuinely needs a flexible search or expression capability, reduce the language surface. Expose a small set of application-defined operations, parse the input into a safe internal representation, validate it, and generate the final interpreter-specific operation from trusted components.
That is safer than accepting a raw expression and trying to remove every dangerous token afterward.
Why escaping alone is a fragile primary defense
Escaping is context-specific. SQL, shell commands, HTML, LDAP filters and regular expressions do not share one universal set of metacharacters.
This creates several common failure modes:
- the wrong encoder is used for the target interpreter;
- input is decoded or transformed after validation;
- escaping happens twice or not at all on one code path;
- a new syntax feature introduces characters the old filter did not consider;
- the application blocks a few suspicious strings but leaves equivalent syntax available;
- a value is safe in one context and dangerous when reused in another.
OWASP therefore prefers safer APIs and parameterization where they exist. Escaping can still be necessary in some contexts, but it should be the correct escaping mechanism for that exact interpreter and should not be treated as a generic security sanitizer.
A denylist of “bad characters” is especially weak because it asks the application to predict every syntax an attacker might use. An allowlist can be useful when the legitimate value space is narrow, such as a country code, sort order or known command option.
Injection is not just an input-validation bug
It is common to hear “validate all input” as the complete answer to injection. Validation is important, but that advice misses the more fundamental control.
Consider a person’s surname containing an apostrophe. Rejecting the apostrophe would make some unsafe SQL strings harder to exploit, but it would also reject legitimate data. A correctly parameterized query does not need to pretend that ordinary punctuation is malicious.
The stronger model is:
- define the expected type and shape of input;
- keep untrusted values separate from interpreter syntax;
- use the interpreter’s safe API or parameter mechanism;
- allowlist genuinely dynamic structural choices;
- run the interpreter with the least privilege it needs;
- handle errors without exposing unnecessary internal details.
Validation and parameterization solve different problems. Good systems use both for the jobs they are actually suited to.
Least privilege limits the damage when prevention fails
MITRE’s guidance for both SQL injection and command injection emphasizes least privilege as a defense-in-depth measure.
A web application’s database account rarely needs unrestricted administrative permissions. If it only reads product data and updates orders, its database identity should not also be able to create new database administrators or access unrelated schemas.
The same applies to processes that invoke operating system tools. A web worker performing image conversion should not run as a privileged system account merely because doing so is convenient.
Least privilege does not fix injection. It changes the consequences if an injection flaw survives review and testing.
This connects injection prevention to broken access control. Even perfectly parameterized code can still expose data if authorization is wrong, and least privilege at the database or OS layer can provide another boundary when application logic fails.
How should developers test for injection?
OWASP Top 10:2025 recommends combining source-code review with automated testing, including testing of parameters, headers, URLs, cookies and structured request data. Static, dynamic and interactive application security testing can all contribute, but none should replace understanding how the application builds interpreter inputs.
A useful review starts with sinks, the places where code talks to interpreters:
- database query execution;
- process and shell execution;
- template or expression evaluation;
- LDAP queries;
- dynamic script execution;
- browser DOM operations that interpret HTML or script.
Then trace backward. Which values can reach each sink? Are they parameters, typed values or raw strings? Can an attacker influence structural parts of the instruction? Does a helper function silently concatenate strings? Does an ORM call accept raw query fragments?
This data-flow approach is often more reliable than searching only for famous payload strings.
Security tooling can help find suspicious patterns, especially dynamic construction around sensitive APIs. The broader practice of reviewing generated code rather than trusting plausible-looking output is also important when using AI coding assistants.
Common misconceptions
“We use an ORM, so SQL injection is impossible”
An ORM can reduce risky string construction, but many ORMs provide raw-query APIs, dynamic filters or escape hatches. OWASP Top 10:2025 explicitly notes that unsafe ORM search parameters can still contribute to injection flaws. Review how the ORM is used, not just whether one is installed.
“The web application firewall will block it”
A WAF may block some known patterns and provide useful defense in depth, but it does not repair unsafe query construction. Encoding differences, alternate syntax and application-specific behavior make request filtering an unreliable replacement for secure code.
“We sanitise the string once at the API gateway”
Safety is context-dependent. A string accepted by an API gateway may later enter SQL, HTML, a shell command and a log parser, each with different syntax. The component that constructs the interpreter input must use the right structural defense for that interpreter.
“Only form fields matter”
Untrusted input can arrive through URL parameters, HTTP headers, cookies, JSON bodies, uploaded files, message queues, database records, third-party APIs and other sources. Stored data can become dangerous later if a different component treats it as code or query syntax.
This is why security misconfiguration and unsafe data flows often interact with injection weaknesses in real systems.
A practical injection review checklist
When reviewing an application, ask these questions in order:
- What interpreters does the application reach? Identify databases, shells, template engines, browsers, directory services and expression engines.
- Where are the sinks? Find the APIs that execute queries, commands, expressions or rendered markup.
- Which parts are controlled by the application? The command or query structure should come from trusted code whenever possible.
- Which parts are externally influenced? Trace request data, stored data and third-party inputs into each sink.
- Can values be bound separately? Use parameterized or typed interfaces rather than string concatenation.
- Does structure genuinely need to be dynamic? If so, map a small allowlisted choice to known-safe server-side structures.
- Can the interpreter be removed? A filesystem, networking or image-processing library may remove the need for a shell command entirely.
- What privileges does the interpreter have? Limit database, OS and service permissions so one flaw does not become full compromise.
- Are errors and logs safe? Detailed interpreter errors can reveal useful internal information, while logs must not capture secrets.
- Do tests exercise hostile and malformed inputs? Combine code review with automated tests and authorized security testing.
The design principle worth remembering
The most useful way to understand injection is not as a list of payloads. It is as a failure to preserve the boundary between data and instructions.
SQL injection happens when data changes SQL. Command injection happens when data changes a command or its arguments. Other injection families repeat the same mistake in different languages and interpreters.
Good defenses therefore begin before filtering: choose APIs that represent data as data, avoid unnecessary interpreters, constrain the pieces that genuinely must be dynamic, and run every downstream component with the minimum authority it needs.
That approach remains useful even as frameworks and attack techniques change. It turns injection prevention from a collection of string tricks into a repeatable secure-design habit.
For a broader view of how injection fits alongside access control, cryptographic failures and other application risks, see OWASP Top 10:2025 Explained with Practical Examples.