Safeguard
Security Guides

OWASP A03: Injection Explained — A Deep-Dive Guide

Injection ranks #3 in the OWASP Top 10 (2021) and now includes XSS. A deep dive into SQLi, command injection, real CVEs, and how to detect and fix it in 2026.

Daniel Osei
Security Researcher
6 min read

Injection happens when untrusted input is sent to an interpreter — a database, a shell, an LDAP directory, a template engine — in a way that lets an attacker change the command being executed rather than merely supplying data to it. It ranks number three on the OWASP Top 10 (2021), down from the top spot it held for years, and the 2021 revision folded Cross-Site Scripting (XSS) into the category because XSS is fundamentally injection into an HTML or JavaScript interpreter. This guide explains the mechanics of injection, why it dropped to third, the CVEs that define it, and the single design principle that prevents nearly all of it.

What OWASP A03 actually covers

A03:2021 spans 33 CWEs, the most of any Top 10 category, including CWE-89 (SQL Injection), CWE-78 (OS Command Injection), CWE-79 (Cross-Site Scripting), CWE-94 (Code Injection), CWE-90 (LDAP Injection), and CWE-917 (Expression Language Injection). The common thread is a broken boundary between code and data. When an application builds a command by concatenating strings — a SQL query, a shell invocation, an HTML response — and one of those strings comes from the user, the interpreter cannot tell where the developer's intent ends and the attacker's begins. Every injection variant is the same bug expressed against a different interpreter, which is why the same fix — keep code and data on separate channels — works across all of them.

Why it ranks number three

Injection ranked first for over a decade and dropped to third in 2021 not because it became rare but because defenses became mainstream. Parameterized queries, ORMs, and framework-level output encoding are now defaults in most stacks, so the average incidence rate fell even though the maximum impact stayed catastrophic. OWASP's 2021 data still recorded injection in 94% of tested applications at some rate, and the category retains one of the highest counts of mapped CVEs. The lesson is that injection is a solved problem in theory and a persistent problem in practice: the moment a developer reaches for string concatenation to build a query "just this once," or a legacy code path escapes the ORM, the bug returns.

Real-world examples

The MOVEit Transfer breach (CVE-2023-34362) was a SQL injection flaw that the Cl0p ransomware group exploited at scale in mid-2023, compromising thousands of organizations and tens of millions of individuals through a single vulnerable managed file-transfer product. Log4Shell (CVE-2021-44228) was an injection into Log4j's JNDI lookup feature: an attacker who could get a crafted string into any logged field triggered a lookup that fetched and executed remote code, and because logging is everywhere, so was the exposure. Further back, the Struts REST plugin flaw (CVE-2017-9805) allowed remote code execution through crafted XML that the framework deserialized and evaluated. Each shows injection's defining trait — a data field treated as executable instructions.

Vulnerable versus fixed code

SQL injection through string concatenation remains the canonical example.

# VULNERABLE: user input concatenated into the query (CWE-89)
def get_user(conn, email):
    cursor = conn.cursor()
    query = "SELECT * FROM users WHERE email = '" + email + "'"
    cursor.execute(query)  # email = "' OR '1'='1" returns every row
    return cursor.fetchall()
# FIXED: parameterized query keeps input on the data channel
def get_user(conn, email):
    cursor = conn.cursor()
    cursor.execute(
        "SELECT * FROM users WHERE email = %s",  # placeholder, not concatenation
        (email,),  # driver binds the value safely
    )
    return cursor.fetchall()

In the fixed version the query text is fixed at author time and the database driver binds email as a pure value. No matter what characters the input contains, it can never become part of the SQL command structure. The same principle applies everywhere: use prepared statements for SQL, argument arrays instead of shell strings for OS commands, and context-aware auto-escaping for HTML.

Prevention checklist

  • Use parameterized queries or a well-configured ORM for every database access; never concatenate input into SQL.
  • Pass OS commands as argument arrays, never as a single shell string, and avoid invoking a shell at all where possible.
  • Rely on framework auto-escaping for HTML output to prevent XSS, and set a strict Content-Security-Policy.
  • Validate input against an allowlist of expected formats as defense in depth, not as the primary control.
  • Disable dangerous interpreter features such as JNDI lookups, XML external entities, and template evaluation of user data.
  • Apply least privilege to database accounts so a successful injection cannot read or drop unrelated tables.
  • Keep interpreters and parsing libraries patched, since injection often lives in a dependency rather than your code.

How Safeguard helps

Safeguard detects injection across your own code and your dependencies, then determines whether it actually matters. Our SCA engine flags vulnerable interpreters and parsers — the Log4j, Struts, and database drivers that carry known injection CVEs — while reachability analysis traces whether the vulnerable function is invoked from an entry point an attacker can reach, so you are not chasing dormant findings. Safeguard DAST actively probes live endpoints with injection payloads to confirm exploitable paths that static analysis alone would only suspect, and Griffin AI reasons across the code path, the dependency graph, and runtime context to rank which injection points sit on your real attack surface. When a fix is clear, Auto-Fix opens a pull request that parameterizes the query or upgrades the vulnerable library. You can run the same checks locally in CI with the Safeguard CLI.

Frequently Asked Questions

Why did XSS get merged into the Injection category?

Because cross-site scripting is injection into the browser's HTML and JavaScript interpreters — the attacker supplies data that the page treats as executable markup or script. Grouping it with SQL and command injection in 2021 reflects that they share one root cause and one class of fix: keep untrusted data out of the code channel and encode it for the context it lands in.

Isn't using an ORM enough to stop SQL injection?

An ORM prevents most SQL injection because it parameterizes queries by default, but it is not a guarantee. Raw query escape hatches, string-built WHERE fragments, and dynamic column or table names passed as strings all reintroduce the risk. Treat the ORM as a strong default and still review any code path that constructs query fragments from input.

What is the difference between injection and insecure deserialization?

Injection sends attacker-controlled data to an interpreter that executes it as commands. Insecure deserialization reconstructs objects from untrusted serialized data, which can trigger code execution during the rebuild. They overlap in impact but differ in mechanism, and OWASP tracks deserialization primarily under A08, Software and Data Integrity Failures.

Can a web application firewall reliably block injection?

A WAF adds a useful layer by blocking known payload patterns, but it is a filter, not a fix — attackers routinely bypass signatures with encoding and obfuscation. The reliable control is architectural: parameterized queries, safe command APIs, and output encoding in the application itself. Treat a WAF as defense in depth, never as the primary defense.

Ready to find injection paths before an attacker does? Start scanning at app.safeguard.sh/register, or read the setup guide at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.