Skip to content
back to the map
technique
stage: code
3 min read

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.

sqlmap (on systems you own)SemgrepCodeQLOWASP ZAPBurp SuiteORM / query builders

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.

users.pypython
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"
users.jsjavascript
const rows = await db.query(`SELECT id, email FROM users WHERE name = '${req.query.name}'`);
exec(`ping -c 1 ${req.query.host}`, cb);
UserDao.javajava
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.

users.pypython
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).stdout
users.jsjavascript
const { 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);
UserDao.javajava
String sql = "SELECT id, email FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setString(1, name);
    ResultSet rs = ps.executeQuery();
}
users.gogo
rows, err := db.QueryContext(ctx, "SELECT id, email FROM users WHERE name = $1", name)
XSS: escape on output (Jinja2 autoescape is on; never mark user input safe)html
<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.

test_injection.py (pytest)python
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 "&lt;script&gt;" in body
static-check.shbash
# 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.
OWASP Top 10The best-known reference list of web application security risks (A01 broken access control, A02 cryptographic failures, A03 injection...), revised every few years by the OWASP community. It is an educational map of risk categories, not a tool checklist or a certification.see in glossary →XSScross-site scriptingInjection into a web page: untrusted input is rendered as HTML or script in another user's browser. Escape on output by context and add a Content-Security-Policy.see in glossary →SQL injectionSQLiInput concatenated into a SQL string is parsed as part of the query. Parameterized queries send the values separately and remove the problem.see in glossary →Defense in depthIndependent layers of protection, on the premise that any one of them can fail: validation + parameterization + WAF + monitoring.see in glossary →Least privilegeleast privilegeThe principle of giving each identity only the permissions strictly needed for its function, and nothing more. It is the opposite of 'admin user by default': every extra role is one more path an attacker can use after a compromise.see in glossary →SASTStatic analysis: examines the code without running it. It finds issues through flow analysis, not guesswork.see in glossary →DASTDynamic testing: attacks the running application from the outside, as a black box.see in glossary →

connections on the map

updated on 14/10/2026 · sources to go deeper · report an error

in other domains

test what you learned

Loading questions…