Back to Blog
OWASP

Injection (OWASP A03): Examples, Detection, and Fixes in 2026

April 25, 202622 min readSecureCodingHub Team
query::parameterize()

Injection is the oldest, most studied, most documented vulnerability class in application security — and it remains, in 2026, one of the top three categories on the OWASP Top 10 at A03. The reason is not that the prevention is unknown; the reason is that injection is not one bug but an entire family of related bugs across SQL, NoSQL, OS commands, LDAP, XPath, templates, ORMs, and GraphQL — each with the same underlying pattern, each requiring the same underlying discipline, and each requiring developers to recognize the pattern in the specific code shape their stack ships. Owasp injection as A03 absorbed cross-site scripting in the 2021 reorganization, broadening an already-broad category. This guide walks the anatomy that unifies every injection class, the variants developers actually meet in production code, the parameterization-and-validation playbook that prevents them, and the detection coverage that catches what slips through.

Why Injection Stays in the OWASP Top 10

Injection topped the OWASP Top 10 from the list's earliest revisions through 2017. The 2021 revision dropped it from #1 to #3 — partly because broken access control had become demonstrably more prevalent, partly because the reorganization grouped XSS into the injection category, which broadened the class without changing its underlying risk. The 2025 list keeps injection at A03, and the consensus in the appsec community is that the ranking reflects not a decline in injection's importance but the genuine elevation of access-control failures above it. Injection is no longer the most prevalent OWASP category on every list; it remains one of the three most prevalent, the one with the deepest research literature, and the one where the gap between "the fix is well-known" and "the fix is consistently applied" is widest.

The persistence of injection in the top three has a specific explanation: the category is defined by a pattern that recurs in many languages, frameworks, and contexts, and each context has its own escape hatch where the pattern reappears. A team that has internalized parameterized SQL queries through ORM use will still write a raw SQL fragment with concatenated input when the ORM does not support a specific operation. A team that has switched to a NoSQL database without query strings will still build a MongoDB query object from unvalidated request data and inadvertently allow operator injection. A team that uses a templating engine to render user-controlled fields will still pass a user message into a server-side template renderer when a feature requires conditional content. The pattern is the same; the surface where it appears is different. Each new technology brings a new injection variant, and the variant is rediscovered case-by-case rather than recognized as the same underlying bug.

The 2021 absorption of XSS into injection is conceptually correct — XSS is, formally, injection of untrusted data into an HTML/JavaScript interpreter — but operationally the category remained large. XSS gets covered separately in most curricula because its mitigation patterns (output encoding, CSP, framework-level escaping) differ enough from server-side injection patterns that treating them as a single discipline often confuses rather than clarifies. This guide focuses on the server-side injection variants — SQL, NoSQL, command, LDAP, XPath, template, ORM, GraphQL — and treats XSS as a related category covered elsewhere.

The Anatomy of an Injection Attack

Every injection vulnerability — across every variant — reduces to a single pattern: untrusted data is interpreted as code, query, or command by a downstream interpreter, when the developer's mental model treated the data as inert content. The interpreter executes the data as instructions instead of treating it as data; the attacker, who supplied the data, has effectively written code that runs in the context of the application.

The pattern decomposes into four elements that every injection vulnerability shares. First, an untrusted source — request parameters, headers, cookies, file uploads, third-party API responses, message-queue payloads, anything not under the application's control. Second, a downstream interpreter — a SQL engine, a MongoDB driver, an OS shell, an LDAP server, a template renderer, an XPath evaluator. Third, a concatenation or composition step in which the untrusted data is combined with a code or query string to produce the final input the interpreter sees. Fourth, the interpreter's parsing of the combined input as a structured language in which the boundary between code and data depends on the data's content rather than on a separate channel.

The reason injection is so common across stacks is that the third and fourth elements are inherent to how most interpreters work. SQL parses a string and recognizes keywords, operators, and literal values based on what the string contains; the SQL engine has no separate channel for "this part is a literal value, do not parse it as SQL." When the developer concatenates user input into the query string, the engine cannot distinguish data from code — and any character the engine treats as syntactic (a quote, a semicolon, a comment marker) becomes an injection vector. The same is true for shell command lines, for LDAP filter strings, for XPath queries, for template strings — every interpreter that takes a single string and parses both code and data from it shares the structural vulnerability.

The remediation pattern follows from the diagnosis: provide the interpreter with a separate channel for code and data, so the boundary is enforced by the protocol rather than by the contents of the string. SQL has prepared statements (the query string with placeholders, the parameter values bound separately, the engine guaranteed to treat parameters as values). Shell commands have argument arrays (the command and its arguments passed as a list, the shell never reparsing the arguments). LDAP has search filters with parameterized values. The pattern is the same across categories: separate the code channel from the data channel and use the protocol's parameterization, not string concatenation.

Understanding this anatomy is what turns a developer from someone who fixes individual injection bugs into someone who recognizes the [Pattern] before it ships. The goal of secure-coding fluency is not to memorize every injection variant but to recognize, in any new context, when untrusted data is being concatenated into a string that a downstream interpreter will parse — and to reach for the parameterization mechanism the protocol provides.

SQL Injection — Still the Class That Pays Out

SQL injection is the most documented injection variant, the one with the deepest tooling, and — somewhat surprisingly — still the one that produces the largest individual incident payouts in 2026. The reason is not that teams write blatantly vulnerable SQL routinely; it is that ORM use has displaced direct SQL writing without eliminating the SQL surface, and the residual SQL surface is where the bugs now concentrate.

Classic SQL injection. The textbook pattern: a request parameter is concatenated into a SQL string, a quote in the parameter terminates the literal, and the rest of the parameter is parsed as SQL. SELECT * FROM users WHERE name = '' OR 1=1 --' returns every row. The attack variants — UNION-based to extract from other tables, error-based to leak schema information through forced errors, blind/time-based to extract data through Boolean side channels when no output is visible — all derive from the same underlying flaw. The fix is a parameterized query: the SQL string contains a placeholder, the parameter is bound separately, and the engine treats the parameter as a literal value regardless of its contents.

The vulnerable pattern in Python:

cursor.execute(
    "SELECT * FROM users WHERE name = '" + name + "'"
)

The fixed pattern, with parameterization:

cursor.execute(
    "SELECT * FROM users WHERE name = %s",
    (name,)
)

The diff is small. The behavior is fundamentally different — the engine in the second form treats name as a value to compare against, with no possibility of reinterpretation as SQL. This is the canonical injection fix, and the same pattern translates to every database driver in every language.

ORM injection through raw query escape hatches. Modern ORMs — SQLAlchemy, Sequelize, Prisma, Hibernate, Entity Framework, ActiveRecord — generate parameterized queries by default. The developer writes User.where(name: name) and the ORM produces a parameterized query. The injection risk reappears when the developer uses the ORM's escape hatch for queries the ORM does not support natively. SQLAlchemy's text(), Django's raw(), ActiveRecord's find_by_sql, Sequelize's query(), Hibernate's createNativeQuery — each takes a SQL string, and each becomes a vulnerability when the developer concatenates input into that string instead of using the parameter-binding API the escape hatch also provides.

The 2026 pentest pattern is consistent: the application has 200 ORM calls, 198 of which are parameterized correctly through the ORM's high-level API, and 2 of which use the raw-query escape hatch with concatenated input. The 2 are the SQL injection findings. The remediation is to ensure the raw-query escape hatch, when used, also goes through parameter binding — every ORM's raw-query API supports it, and the discipline is to never reach for the escape hatch without also reaching for its parameterization.

Second-order SQL injection. A variant where the malicious input is stored in the database through a sanitized path (registration form with input validation, for example) and is then concatenated unsanitized into a query later — typically in an admin panel, a reporting tool, or an internal search. The validation at the original input boundary did not prevent storage of the payload; the assumption that "data from the database is safe to concatenate" produces the injection at the second use. The remediation is to treat data as untrusted regardless of its source — every concatenation into a query string is an injection vector, whether the data came from the request directly or from the database.

Stored procedures are not automatically safe. A common misconception is that SQL injection is impossible in code that uses stored procedures. The misconception fails because a stored procedure that internally builds a dynamic SQL string from its parameters and executes it is just as vulnerable as inline SQL. The fix is the same: parameterize the dynamic SQL inside the stored procedure, or restructure to avoid dynamic SQL entirely.

NoSQL Injection — The Class Most Backends Ship Without Knowing

NoSQL databases displaced relational databases in many architectures over the last decade, and the migration brought a quiet assumption: "without SQL strings, we can't have SQL injection." The assumption is technically correct and operationally misleading. NoSQL has its own injection variants, and they are widespread in 2026 backends — frequently in code shipped by teams that explicitly switched to NoSQL to avoid injection risk.

MongoDB operator injection. The most common NoSQL injection variant. MongoDB queries are JSON objects, and operators ($gt, $ne, $where, $regex, $exists) are JSON keys that change the query's behavior. When a backend builds a query object directly from request parameters — without restricting the keys to the expected set — an attacker can inject operators and bypass authentication or extract data.

The vulnerable pattern in Node.js/Express:

// Request body: { username: "admin", password: { "$ne": null } }
const user = await db.collection('users').findOne({
  username: req.body.username,
  password: req.body.password
})

The attacker passes a password object that contains the $ne (not equal) operator with a null value. MongoDB interprets the query as "find a user named admin whose password is not null" — every admin matches. The login succeeds without knowing the password.

The fix combines schema validation with explicit type coercion:

// Validate that fields are strings, not objects
const { username, password } = req.body
if (typeof username !== 'string' || typeof password !== 'string') {
  return res.status(400).send('Invalid input')
}
const user = await db.collection('users').findOne({
  username: username,
  password: password
})

Schema validation libraries — Zod, Joi, Yup, ajv — encode the expected type for each field and reject any request that does not match. Adopting one across the codebase eliminates this category of NoSQL injection by construction.

Query language injection in Cassandra, Couchbase, and Elasticsearch. Each NoSQL database with its own query language has its own injection variant. Cassandra's CQL is close enough to SQL that classic SQL-injection patterns apply. Couchbase's N1QL has the same injection profile as SQL. Elasticsearch query DSL has operator-injection variants similar to MongoDB. The common thread: any database that accepts a structured query and exposes a way to compose that query from input strings has an injection class. The mitigation is the same pattern as SQL — parameterize through the driver's API, validate the schema of structured input, and never compose query strings by concatenation.

JSON parameter injection. A broader category that overlaps NoSQL but extends to any backend that takes a JSON request and passes its fields through to a downstream system without schema validation. Authorization claims, role identifiers, tenant scopes, billing amounts — any field that should be drawn from server-side state but is instead trusted from the client request. The injection is not into a query language; it is into the application's trust model. The mitigation is the same — schema validation on every boundary, and never trusting client-supplied claims for security-relevant decisions.

Command Injection and OS Command Pipelines

Command injection occurs when an application invokes an OS shell or external program and concatenates input into the command line. The shell parses the resulting string, recognizes shell metacharacters (;, |, &, backticks, $()), and executes additional commands the developer never intended. The class is rarer than SQL injection in modern applications because most code does not invoke shells, but where it appears the impact is typically remote code execution — the highest-severity outcome of any injection class.

The shell=True pattern. Python's subprocess module accepts a shell=True argument that passes the command through a shell rather than executing it directly. With shell=True, the shell parses the command line, and any input concatenated into the string becomes shell-parseable. With shell=False (the default in modern code), the program is executed directly with arguments, and shell metacharacters are passed as literal characters to the program rather than being interpreted by a shell.

The vulnerable pattern:

import subprocess
filename = request.GET.get('filename')
subprocess.run(
    f"convert {filename} output.png",
    shell=True
)

An attacker supplies a filename of image.jpg; rm -rf /; the shell parses the semicolon, runs the convert command, and then runs the destructive command. The fix is to pass arguments as a list, not a string, with shell=False:

subprocess.run(
    ["convert", filename, "output.png"],
    shell=False
)

The arguments are passed to the convert program directly. The shell is never involved. Shell metacharacters in the filename are passed to convert as part of the filename argument and are never interpreted as shell syntax. The same pattern applies to Node.js (child_process.execFile and spawn with an array, not exec with a string), Ruby (Process.spawn with an array), and every other language with a subprocess API.

Filename and path injection. Even without a shell, command injection variants appear when filenames or paths supplied by users are passed to programs that interpret them specially. ImageMagick's older versions interpreted filenames starting with | as commands to execute (the ImageTragick vulnerability). Tar archives interpret filenames specially. PDF generators that take input filenames may interpret special path syntax. The general defense is path normalization, allowlist validation of the filename's character set, and restriction of the input to a known safe directory. The specific defense depends on the program; the mitigation pattern is to research the program's input handling rather than assume safe defaults.

Indirect command injection through environment variables and config. A subtler variant: the application accepts an environment variable, configuration value, or feature flag from a partially trusted source, and that value is interpolated into a shell command later. The injection appears not at the request boundary but at the configuration boundary. The defense is to apply the same validation discipline to configuration sources as to request inputs — every external input is untrusted regardless of the channel it arrives on.

Template Injection (SSTI) — When Render Becomes RCE

Server-Side Template Injection — SSTI — is an injection variant that has gained prominence as templating engines have become more powerful and more universally adopted. The pattern: an application takes user-controlled input and passes it into a server-side template renderer that supports expressions or code execution. The renderer executes the user's input as template code rather than rendering it as data.

The most common path: a developer wants to support user-customized email subjects or report templates and passes the user's input directly into the engine's render function. Jinja2, Handlebars, Velocity, Twig, Pug, FreeMarker, ERB — each supports expression evaluation, and each becomes an RCE vector when user input reaches the rendering function unprotected.

The vulnerable pattern in Python with Jinja2:

from jinja2 import Template
user_message = request.GET.get('message')
template = Template(f"Hello, {user_message}")
output = template.render()

The user supplies a message of {{ 7*7 }}; the engine evaluates the expression and outputs 49. From there, the attacker explores the engine's expression syntax — Jinja2 exposes Python objects, Handlebars exposes JavaScript helpers — and reaches arbitrary code execution. The fix is to never compile templates from user input. Pass the user input as a variable to a fixed template:

template = Template("Hello, {{ message }}")
output = template.render(message=user_message)

The template is fixed at deploy time. The user input is data passed to the template engine through the variable channel; the engine treats it as text and applies the engine's auto-escape (when enabled). The pattern translates to every engine — the rule is "templates are code, written by developers, version-controlled; user input is data, passed as variables, never concatenated into the template source."

Auto-escape is necessary but not sufficient. Most modern template engines auto-escape output by default to prevent XSS. Auto-escape addresses the cross-site scripting variant of injection but does not address SSTI — the injection where the user's input becomes part of the template itself rather than part of the rendered output. SSTI is a higher-severity class than the XSS auto-escape protects against; it requires the architectural discipline of never compiling templates from user input.

Markdown, BBCode, and "safe" markup. Markdown renderers, BBCode parsers, and similar markup engines are typically safe in their default configurations but become vulnerable when extended with custom tags, when raw HTML is enabled in user input, or when the renderer's output is post-processed in unsafe ways. The mitigation is to use the renderer's strict mode, disable raw HTML in user-supplied content, and audit any extensions for injection risk.

LDAP, XPath, ORM Lazy-Loading, and GraphQL — The Long Tail

The major injection categories — SQL, NoSQL, command, template — capture the majority of injection findings. The long tail of injection variants accounts for the remainder, and is worth covering because each category appears regularly enough in production code that recognizing the pattern is worth the paragraph.

LDAP injection. Applications that authenticate against an LDAP directory or query LDAP for group membership build LDAP filter strings. The filter string has its own syntax — parentheses, asterisks, ampersands — and concatenated input becomes injectable. An attacker who supplies a username of *)(uid=* can construct a filter that matches every entry, bypassing the username constraint. The fix is to use the LDAP driver's parameterized search API or to escape filter metacharacters before concatenation.

XPath injection. Applications that query XML documents with XPath expressions build XPath strings. Like LDAP, XPath has its own syntax, and concatenated input opens the same injection class. The fix is XPath variable binding (XPath 2.0+) or careful escaping of XPath metacharacters in the input. XML-based applications with user-controlled queries should also consider the broader XXE (XML External Entity) attack class, which is a related but distinct injection-adjacent vulnerability.

ORM lazy-loading and N+1 query injection. A subtler ORM-related variant: an application uses an ORM relationship that lazy-loads child resources, and the lazy-load triggers additional queries with parameters drawn from the parent. If the relationship's join condition includes a user-controlled field — through misconfiguration of the ORM mapping — the lazy-load query can be injectable. The class is rare but appears in audits of older ORM-heavy codebases. The mitigation is to review ORM relationship definitions for any user-controlled fields in join conditions.

GraphQL injection. GraphQL is, at the protocol level, a structured query language that does not have classic injection because the query and variables are separate channels. The injection variants that appear in GraphQL are at the resolver level — a resolver that takes a variable and concatenates it into a downstream SQL query, NoSQL query, or shell command, reintroducing the underlying injection class through the resolver. The mitigation is the same as for any other backend code path: the resolver must use parameterized queries for downstream calls, regardless of the fact that the GraphQL layer above it is structurally safe.

Header injection and CRLF injection. Applications that set HTTP response headers from user input can be vulnerable to CRLF injection — where a carriage-return-line-feed sequence in the input lets the attacker terminate the header and inject additional headers, including response-splitting attacks that produce two responses from one request. The mitigation is to reject CR and LF characters in any input that flows into a response header.

The Mitigation Playbook

The variants above converge on a small set of mitigation patterns that, applied consistently, eliminate the vast majority of injection risk. The patterns are not new in 2026; the engineering discipline is in applying them across every code path that combines untrusted input with a downstream interpreter.

Parameterize at the protocol level. Every interpreter the application talks to provides a parameterization mechanism: prepared statements for SQL, parameterized search for LDAP, argument arrays for subprocess, variable binding for templates, schema validation for JSON. The default code path uses the parameterization mechanism. Code that bypasses it — the ORM raw-query escape hatch, the shell=True subprocess call, the Template-from-string template — is reviewed explicitly in code review and justified case by case.

Validate input at the boundary. Every untrusted input has an expected type, length, format, and character set. The validation runs at the application boundary — request parsing, message-queue handler, file upload handler — and rejects inputs that do not match. The validation is not a substitute for parameterization (a correctly validated input concatenated into a query is still injectable in some cases) but is a defense-in-depth that catches the cases parameterization misses and shrinks the attack surface for the cases parameterization handles.

Schema-based input validation. Validation libraries — Zod (TypeScript), Joi/Yup, ajv (Node.js), Pydantic (Python), MarshMallow, Cerberus, Bean Validation (Java), FluentValidation (.NET) — express the expected schema for each input as code, validate the input against the schema, and reject mismatches. Adopting a schema validation library across the codebase eliminates an entire class of NoSQL operator injection, JSON injection, and trust-boundary failures.

Output encoding for context-specific sinks. When the application produces output that flows into another interpreter — HTML for browsers, JSON for downstream systems, SQL for queries, shell for commands — the output is encoded for the specific sink. HTML encoding for HTML, JSON encoding for JSON, prepared-statement parameters for SQL, argument arrays for shell. The encoding is sink-specific; a string safe for HTML is not safe for SQL, and a string safe for SQL is not safe for shell.

Authorization-aware query builders. A pattern that connects injection prevention to access control: the application's data access layer takes the authenticated session as input and constructs queries that automatically include the appropriate authorization filters. The query builder ensures that every query is both parameterized (preventing SQL injection) and authorization-scoped (preventing IDOR through SQL). The two disciplines reinforce each other, and the combined pattern eliminates more risk than either applied alone.

Default-deny configuration of interpreters. Where interpreters offer dangerous features that are rarely needed, disable them by default. Disable MongoDB's $where operator at the driver or database level if the application does not use it. Disable XML external entity processing in XML parsers. Disable raw HTML in markdown renderers. Disable expression evaluation in template engines used for emails. The investment is in the configuration; the return is that the dangerous feature is unavailable to be misused, regardless of subsequent code changes.

Connection-level protections. A defense-in-depth that survives application-layer injection: the database account the application connects with has the minimum privileges needed for the application's queries. The application user cannot drop tables, cannot read system catalogs, cannot access tables outside its scope. A successful SQL injection against an application with strict database privileges produces less damage than against an application with broad ones. The same pattern applies to message queues, file systems, and downstream APIs — every connection runs as a least-privileged principal. This connects to the broader configuration discipline that prevents weak defaults like default DB credentials, broad service-account scopes, and unscrubbed query logs from amplifying any successful injection.

Code review with an injection lens. Every pull request that touches code constructing a query, command, template, or other interpreted string includes an explicit review for injection — the parameterization mechanism is in use, the input validation is present, the connection is least-privileged. Secure code review with an injection-specific checklist consistently catches the class; review without one consistently misses it.

Detection: SAST, DAST, IAST, and Runtime

Detection of injection vulnerabilities runs across multiple complementary tools. Each catches a different slice; no single tool catches everything. The combinations that produce the strongest coverage are well-understood, and the tradeoffs are documented in our IAST vs DAST vs SAST comparison guide.

SAST — Static Application Security Testing. Source-code scanners trace data flow from input sources to interpreter sinks. SAST catches the syntactic patterns reliably — concatenation into a SQL string, shell=True with input, raw-query escape hatch with concatenated input, Template from string. The tool reports the path from source to sink and the developer reviews the finding. SAST's strength is exhaustive code coverage; its weakness is false positives (sources that look like inputs but are not, sinks that look unsafe but have upstream sanitization the scanner cannot prove). Modern SAST — CodeQL, Semgrep, Snyk Code, SonarQube — handles the common cases well and produces noise on edge cases.

DAST — Dynamic Application Security Testing. Black-box scanners send injection payloads at the running application and observe the responses. DAST catches the runtime behavior — the actual response when an injection payload reaches an interpreter. Its strength is the absence of false positives (a confirmed injection is confirmed); its weakness is incomplete coverage (DAST sees only the endpoints it knows to test, and only the payloads it knows to send). DAST tools — OWASP ZAP, Burp Suite, Nuclei — are the standard for confirming injection findings before they reach production.

IAST — Interactive Application Security Testing. Instruments the running application and observes the data flow during test execution. IAST combines SAST's source-to-sink tracing with DAST's runtime confirmation, producing high-confidence findings with low false-positive rates. IAST's weakness is the instrumentation overhead and the requirement for representative test traffic. IAST tools — Contrast Security, Seeker, Hdiv — are the strongest single category for injection detection but require investment in test coverage and in the integration with the test environment.

Runtime protection — RASP and WAF. Runtime Application Self-Protection and Web Application Firewalls block injection payloads at request time. RASP runs inside the application and has visibility into the runtime context; WAF runs at the network edge and has only request-level signals. Both produce false positives on legitimate traffic and miss obfuscated payloads, which is why they are defense-in-depth rather than primary mitigation. They are valuable as the last layer of defense for cases where the upstream code-level mitigations failed, but a program that relies on RASP/WAF as the primary defense has misallocated its investment.

Database query log monitoring. A complementary detection that catches injection after the fact: monitor the database query log for anomalous queries — UNION operations the application never issues, schema-introspection queries from application accounts, queries with suspicious comment markers, queries with unusual length or repetition patterns. The monitoring catches injection that succeeded against the application but produced query-log signatures detectable downstream. Combined with alerting and incident response, query-log monitoring shrinks the time-to-detection from "in the breach report" to "in the SOC queue."

The pattern that produces the strongest injection coverage in 2026 is SAST in CI on every pull request, DAST in CI/CD on every staging deployment, IAST in long-running test environments where the cost of instrumentation is amortized, and runtime monitoring on production database queries as the safety net. No single tool is sufficient; the layering is what catches the variants each tool individually misses.

· OWASP A03 · DEVELOPER ENABLEMENT ·

Scanners Find Injection. Developers Stop Writing It.

A SAST tool that flags a concatenated SQL string in CI is better than discovering the vulnerability six months later in a pentest report — but neither is as good as a developer who would never have written the concatenation in the first place. SecureCodingHub builds the parameterization-first, schema-validation-first, escape-hatch-aware fluency that turns injection from a recurring scanner finding into something developers catch themselves at code-review time. If your team is tired of every pentest producing another SQL injection, NoSQL operator injection, or template injection report, we'd be glad to show you how our program changes the input side of that pipeline.

See the Platform

Closing: The Pattern Beneath Every Injection Class

The variants in this guide — SQL, NoSQL, command, LDAP, XPath, template, ORM, GraphQL, header — appear superficially different. The languages are different, the syntax is different, the exploitation techniques are different, the tooling that detects each is different. The underlying pattern is identical. Untrusted data is concatenated into a string. The string is parsed by a downstream interpreter. The interpreter cannot distinguish where data ends and code begins, because the boundary is encoded in the string's contents rather than in a separate channel. The attacker controls a portion of the contents and uses that control to redraw the boundary in a way the developer did not intend.

The mitigation is identical too. Provide a separate channel. Use the protocol's parameterization. Validate the input's schema. Treat every external input — from the request, from the database, from the configuration, from the message queue — as untrusted regardless of channel. Apply the discipline at every boundary where the application composes input into a string a downstream interpreter will parse. The principle is one sentence; the application of it across every code path that touches an interpreter is years of engineering practice.

Injection is ranked A03 on the OWASP Top 10 not because it is the most prevalent finding any longer — broken access control has overtaken it — but because the category continues to produce the highest-severity individual incidents, the most catastrophic data exfiltrations, and the most direct paths from a single application bug to full compromise of the underlying infrastructure. The teams that have largely closed their injection surface in 2026 share a small set of practices: parameterization at the protocol level, schema validation at every boundary, ORM raw-query escape hatches reviewed explicitly, subprocess calls without shells, templates compiled from source files only, least-privilege database connections, and a code-review culture that treats every concatenation into an interpreter-bound string as a finding. None of those practices is exotic; the institutional commitment to apply them consistently is.

The category that has been on the OWASP list every year for two decades, has the deepest research literature, and has the most well-understood prevention is still in the top three for the same reason it has always been there: the prevention is consistent only when the developers writing the code recognize the pattern in their stack's specific shape. That fluency is what secure-coding training is for, and it is the difference between a program that detects injection in CI and a program that no longer ships injection in the first place.