Skip to content

Lesson 1 of 11 · 7 min read

Injection Vulnerabilities

Free lessonReviewed By the AISecurityInterviews editorial team
Reading position
In this lesson

TL;DR: Injection is untrusted data crossing into an interpreter and being parsed as code. It covers SQL, operating-system commands, template engines, LDAP, XPath and more. The fix that works is structural: give the interpreter code and data through separate channels, so data can never change the code's shape. Parameterised queries do that for SQL; argument arrays without a shell do it for commands; sandboxed or logic-less templates do it for rendering. Escaping and blocklists are weaker fallbacks. Language models are an interpreter with no separate data channel, which is why prompt injection cannot be fixed the same way.

Before you read

Pick an answer before you read further. The lesson below explains it, and guessing first, right or wrong, helps the explanation stay with you.

  1. 1A developer escapes single quotes in user input before building a SQL string. Why is this weaker than a parameterised query?

One bug, many interpreters

An interpreter takes text and acts on it: a database runs SQL, a shell runs commands, a template engine evaluates expressions, a browser runs HTML and JavaScript. Injection happens when an application builds that text by combining its own code with data from somewhere less trusted, and the combination lets the data change what the code does.

InterpreterInjectionTypical cause
SQL databaseSQL injectionString concatenation into queries
Operating system shellCommand injectionPassing a built string to a shell
Template engineServer-side template injectionRendering user input as a template, not into one
BrowserCross-site scriptingInserting data into HTML without encoding
LDAP, XPath, NoSQLQuery injectionBuilding queries from strings or objects from input
Language modelPrompt injectionUntrusted text in the same context as instructions

The shape repeats because the cause repeats: one channel carries both code and data, and the boundary between them is decided by parsing.

How common it still is

The OWASP Top 10 2025 places Injection at A05, mapping 37 weaknesses to it with 62,445 CVEs and over 1.4 million recorded occurrences. Cross-site scripting dominates the count (more than 30,000 CVEs, high frequency and usually lower impact) while SQL injection has more than 14,000 (lower frequency, high impact). Injection dropped from first place years ago because frameworks made the safe path the default; it stays in the list because developers keep stepping off that path.

The structural fix: separate channels

The strongest fix is to stop combining code and data in one string.

SQL. A parameterised query sends the statement with placeholders, then the values separately. The database compiles the statement before seeing the values, so no value can add a clause.

import sqlite3

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (name TEXT, email TEXT)")
db.execute("INSERT INTO users VALUES ('alice', '[email protected]')")
db.execute("INSERT INTO users VALUES ('bob', '[email protected]')")

name = "alice' OR '1'='1"  # attacker-controlled

# Vulnerable: the input becomes part of the SQL text.
unsafe = f"SELECT email FROM users WHERE name = '{name}'"
print(db.execute(unsafe).fetchall())  # every row: both emails

# Safe: the statement and the value travel separately.
safe = "SELECT email FROM users WHERE name = ?"
print(db.execute(safe, (name,)).fetchall())  # => []

Commands. Run programs with an argument list and no shell, so metacharacters like ;, | and $( ) are just characters.

import subprocess

filename = "report.pdf; rm -rf ~"  # attacker-controlled

# Safe: no shell parses the string; the whole value is one argument.
subprocess.run(["file", "--", filename], check=False)

The -- ends option parsing, so a value starting with - cannot become a flag. Better still, avoid running external programs on user input when a library call can do the job.

Templates. Render user input as data into a fixed template; never compile user input as a template. Use auto-escaping engines and sandboxed modes for anything user-authored.

The forms that survive parameterised queries

Teams that "use an ORM" or "always parameterise" still ship injection. The survivors share a pattern: somewhere, a string is built after all.

  • Second-order injection. Data is stored safely, then later read back and concatenated into a query by code that assumes "it came from our own database". The injection fires in a different service, hours later, often with more privileges.
  • ORM escape hatches. Raw-query methods, string-built filter fragments and dynamic ORDER BY clauses bypass the ORM's parameterisation. Search a codebase for them; they are where the remaining SQL injection lives.
  • Identifiers. Table and column names cannot be bound as parameters, so a user-chosen sort column must be mapped through an allowlist in code.
  • Stored procedures that build SQL. A procedure is only safe if its body uses parameters; one that concatenates its arguments into dynamic SQL with EXEC or EXECUTE is a SQL injection with an extra hop.
  • NoSQL operator injection. A document database that accepts a JSON query lets an attacker replace a value with an operator object, such as a "not equal to empty" condition in a password field, changing the meaning of the query without any special characters. Validate types: if a field should be a string, reject objects.

Expression and lookup injection

Some interpreters hide inside libraries you would not call interpreters. Two incidents made the point at enormous scale:

  • Log4Shell (CVE-2021-44228, December 2021). Log4j 2 versions from 2.0-beta9 to 2.15.0 evaluated ${...} lookup expressions found in logged data, including JNDI lookups that fetched and loaded remote code. Any user-controlled string that reached a log line, such as a user agent or a username, reached an interpreter.
  • Apache Struts (CVE-2017-5638, March 2017). The Jakarta multipart parser's error handling evaluated an OGNL expression supplied in a request header, giving remote code execution; it was the entry point in the Equifax breach that year.

Both are injection in the classic sense: untrusted text reached an engine that treats some text as code. The defence is the same as for SQL: find the interpreter, keep data out of its code channel (disable lookups, never evaluate expressions built from input) and patch the library.

LESSON · INJECTION VULNERABILITIES
Second-order injection: safe at the door, unsafe later

Where it went wrong

Trusted because it is ours

  • Data from our own table was concatenated

Fix

  • Parameters in the job too
  • Read-only role on one schema
1/3

The first write is safe

Parameters store the payload as plain data. Nothing executes at signup.

Fallbacks, and why they are fallbacks

  • Allowlisting input (only digits for an ID, only known column names for sorting) is strong where the set of valid values is small. Use it for identifiers that cannot be parameterised, such as table or column names.
  • Escaping is correct only for a specific interpreter, context and character set. It is the right tool for output encoding in HTML, and the wrong first choice for SQL.
  • Blocklists of dangerous strings fail against encodings, alternative syntax and inputs the author did not anticipate.

Least privilege as the second layer

Assume one injection will slip through. A database account limited to the tables and operations the application needs turns a full compromise into a small one, as the least privilege lesson shows.

The database's own defaults help if you keep them. SQL Server ships with xp_cmdshell, which runs operating-system commands, disabled on new installations; PostgreSQL's COPY ... PROGRAM needs superuser rights or the pg_execute_server_program role. An application account that holds neither cannot turn a SQL injection into command execution. Row-level security, available in PostgreSQL since 9.5, adds a fail-closed tenant filter inside the database, so a query that forgot its WHERE tenant_id = ? still returns only the current tenant's rows. Blocking outbound network access from database hosts closes the remaining exfiltration and payload-fetching paths.

The bridge to prompt injection

LESSON · INJECTION VULNERABILITIES
Two channels for SQL, one channel for a prompt

Developer's code

Query or instructions

SQL engine

Parses code first

Behaviour

Also in the picture

Untrusted data

  • Form field or document

Language model

  • One token stream
1/2

SQL: separate channels

With a parameterised query the statement is parsed before the values arrive. Data cannot change the code's shape.

SQL injection was solved, in principle, by an API change. Prompt injection has no equivalent API, because a language model reads its instructions and its data in the same way. That is the single most important connection between classic application security and AI security, and it is why AI defences focus on what a manipulated model can reach rather than on stopping the manipulation.

Worked example: a second-order bug, with the blast radius

A user signs up with a display name containing a quote and a SQL fragment. The signup code uses parameters, so the name is stored exactly as typed. That night, a reporting job builds SELECT ... WHERE owner = '<display name>' by string concatenation, because "the data came from our own database". The fragment closes the string and appends its own clause.

Three facts decide how bad this is:

QuestionThis systemBetter
What role does the job run as?A role with read access to every schema and write access to the reporting tablesA read-only role limited to the reporting schema
Can that role run commands?It owns the database, so COPY ... PROGRAM is availableNo superuser, no pg_execute_server_program
Can the database host reach the internet?YesNo outbound access

With the "better" column, the same injection can read reporting data and nothing else. The code fix is parameters in the job; the design fix is the column on the right.

Misconceptions interviewers listen for

  • "Data from our own database is trusted." Its origin was a user; trust is about where data came from first.
  • "We use an ORM, so no SQL injection." Raw queries, string-built filters and dynamic ordering bypass it.
  • "Escaping equals parameterising." Escaping is interpreter- and charset-specific; parameters separate the channels.
  • "Stored procedures are safe." Not when they build dynamic SQL from their arguments.
  • "NoSQL has no injection." Operator objects change query meaning without any quote characters.

What if · interview follow-ups

Hold your answer as the constraints change

You are reviewing a multi-tenant SaaS backend that uses an ORM, a document database for activity feeds, Log4j for logging and an LLM feature that answers questions about the user's data. The interviewer moves through the interpreters one at a time.

  1. Start here

    What if the ORM is used everywhere except a reporting endpoint that calls a raw query with an f-string?

How this shows up in interviews

"What do SQL injection and command injection have in common?" tests whether you see the class rather than memorise instances. Strong answers name the interpreter and the boundary, explain why parameterisation is structural while escaping is contextual, and connect the class to prompt injection with the right caveat. In code review rounds, expect a snippet that builds a query or command from input; fix it with parameters or argument arrays, then mention least privilege.

Say it in 30 seconds: "Injection is untrusted data reaching an interpreter in the same channel as code, so the interpreter parses some of it as instructions. The structural fix is separate channels: parameterised queries, argument arrays without a shell, data rendered into fixed templates. Allowlists help for identifiers; escaping and blocklists are fallbacks. Prompt injection is the same class in an interpreter with no separate data channel, so there the defence shifts to limiting what the model can do."

Check yourself

Say or write your answer first, then reveal. Explaining it from memory, in your own words, is the skill an interviewer is checking.

  1. 1A reporting page lets users choose a sort column, which is inserted into ORDER BY. Placeholders do not work for column names. What do you do?

  2. 2Why is 'escape the model's output before putting it in a SQL query' the wrong fix when an LLM generates query parameters?

  3. 3What makes second-order SQL injection hard to catch in testing?

  4. 4Why was Log4Shell an injection vulnerability?

Practise it as an interviewer asks it

Related lessons