A05 Injection
When data and instructions travel in the same string, whoever controls the data controls the instruction. The cure is to keep them apart, in every language.
What it is
Injection happens when an application builds a command or query by concatenating untrusted input, and an interpreter (a SQL engine, a shell, an LDAP server, a template engine, a browser) cannot tell where the data ends and the instruction begins.
A05 in :2025 covers SQL, NoSQL, OS command, LDAP and template injection, and (XSS), which is injection into the browser's HTML and JavaScript interpreter.
How the attack works
A login query is built as "SELECT * FROM users WHERE name = '" + name + "' AND pass = '" + pass + "'". Entering admin' -- as the name turns the rest of the query into a comment and the password check disappears. With a UNION the attacker reads other tables; with stacked queries, writes them.
- : read, modify or delete data; sometimes reach the operating system.
- OS command injection: input reaches a shell (; cat /etc/passwd, $(...), backticks).
- XSS: input reaches the page unescaped and runs script in another user's session.
- NoSQL and template injection follow the same pattern with different syntax.
Vulnerable code
String concatenation into SQL and into a shell command.
def find_user(db, name):
query = "SELECT id, email FROM users WHERE name = '" + name + "'"
return db.execute(query).fetchall()
def ping(host):
return os.popen("ping -c 1 " + host).read() # host = "x; cat /etc/passwd"const rows = await db.query(`SELECT id, email FROM users WHERE name = '${req.query.name}'`);
exec(`ping -c 1 ${req.query.host}`, cb);String sql = "SELECT id, email FROM users WHERE name = '" + name + "'";
Statement st = conn.createStatement();
ResultSet rs = st.executeQuery(sql);The fix
Parameterize: send the query and the values separately, so the database never parses the value as SQL. For commands, avoid the shell entirely and pass an argument array. For HTML, rely on a templating engine that escapes by default. Validation (allow-lists of shape and length) is a second layer, not the fix.
def find_user(db, name):
return db.execute("SELECT id, email FROM users WHERE name = ?", (name,)).fetchall()
HOST_RE = re.compile(r"^[A-Za-z0-9.-]{1,253}$")
def ping(host):
if not HOST_RE.fullmatch(host):
raise ValueError("invalid host")
return subprocess.run(["ping", "-c", "1", host], capture_output=True, text=True).stdoutconst { rows } = await db.query("SELECT id, email FROM users WHERE name = $1", [req.query.name]);
// no shell: arguments are passed as an array
execFile("ping", ["-c", "1", host], cb);String sql = "SELECT id, email FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
}rows, err := db.QueryContext(ctx, "SELECT id, email FROM users WHERE name = $1", name)<p>Hello, {{ user_name }}</p> {# escaped #}
<p>Hello, {{ user_name | safe }}</p> {# DANGEROUS: do not do this with user input #}Prove the fix
Write the attack as a test. Send the classic payloads and assert that they are treated as plain data: the query returns nothing instead of everything, the shell is never invoked, and script characters come back escaped. Keep these tests forever; they document the threat.
PAYLOADS = ["' OR '1'='1", "admin' --", "'; DROP TABLE users; --", "x' UNION SELECT password,1 FROM users --"]
@pytest.mark.parametrize("payload", PAYLOADS)
def test_sql_payloads_are_just_data(db, payload):
assert find_user(db, payload) == [] # no row has that literal name
assert db.execute("SELECT count(*) FROM users").fetchone()[0] > 0 # table still exists
def test_ping_rejects_shell_metacharacters():
with pytest.raises(ValueError):
ping("example.com; cat /etc/passwd")
def test_html_is_escaped(client):
body = client.get("/hello?name=<script>alert(1)</script>").text
assert "<script>" not in body and "<script>" in body# catch concatenated SQL and shell=True at review time, in any commit
semgrep --config p/owasp-top-ten --error src/Checklist
- Parameterized queries or a vetted ORM for every database call; no string-built SQL.
- Never pass input to a shell. If you must run a program, use an argument array and an allow-list.
- Escape by context on output (HTML, attribute, URL, JavaScript); keep auto-escaping on; add a Content-Security-Policy.
- Validate input by allow-list (type, length, format) as .
- Run the database account with : no DROP, no file access, no admin.
- Scan in CI ( for the pattern, for the runtime behavior) and keep the attack payloads as regression tests.
connections on the map
- A05 Injectioncan bypassA01 Broken Access Control
updated on 14/10/2026 · sources to go deeper · report an error
in other domains
- Web and API security testingoffsechow to test what the other teaches you to fix
- SASTappsecit is the pattern that static analysis looks for in code
- L7 attacks: what to look forsecopsseen from outside: the signatures at the edge
test what you learned
Loading questions…