SQL Injection
Also known as: sqli, sql-injection
A vulnerability where attacker-controlled input is concatenated into a SQL query, letting them rewrite it to read, modify, or destroy any data the app can access.
- Primary domain
- Security & Privacy
- Sub-category
- Hardware, Network, Application & Information Security
In simple terms
SQL injection is when user-supplied text gets glued directly into a SQL query string, allowing the attacker to change what the query does. A login form that builds SELECT * FROM users WHERE name = '$name' lets an attacker enter ' OR 1=1 -- — the query becomes WHERE name = '' OR 1=1 --, which matches every row and bypasses authentication.
The Visual Map
flowchart TD
I["User input"] --> V{"Concatenation?"}
V -->|"Yes — VULNERABLE"| Q1["SELECT * FROM users\nWHERE name='' OR 1=1--"]
V -->|"No — parameterized"| Q2["SELECT * FROM users\nWHERE name=? param: input"]
Q1 --> DB1["DB executes rewritten query\nreturns all rows"]
Q2 --> DB2["DB treats input as literal string\nreturns nothing for ' OR 1=1--"]
DB1 --> BREACH["Data breach / auth bypass"]
DB2 --> SAFE["Safe result"]
More detail
The fix is simple: never concatenate user input into SQL. Use parameterised queries (prepared statements), where the SQL structure and the data are sent to the database separately — the engine never interprets the data as SQL:
-- Vulnerable
query = "SELECT * FROM users WHERE name = '" + name + "'"
-- Safe (parameterised)
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
Every modern database driver supports parameterised queries. Every ORM uses them by default. Writing injectable code in 2026 requires deliberately bypassing these defaults.
Classes of SQL injection:
- In-band — the attacker sees query results in the page.
- Blind — no visible output, but the attacker infers data via page behaviour (timing, true/false redirects).
- Out-of-band — the DB is forced to make a network call (DNS lookup, HTTP request) that leaks data.
What an attacker can do once they have injection:
- Read any data the app can read — usually the whole database.
- Write / modify data; escalate privileges via
UPDATEon the users table. - Execute OS commands via DB features (
xp_cmdshellon SQL Server,COPY ... TO PROGRAMon PostgreSQL). - Pivot to other services on the internal network.
Adjacent injection vulnerabilities follow the same pattern:
- NoSQL injection — against MongoDB/DynamoDB query languages.
- Command injection —
system("$user_input")in shell calls. - LDAP injection, XPath injection, template injection.
Additional defences:
- Least privilege — the app’s DB user shouldn’t be able to
DROP TABLE. - Input validation — numeric IDs as integers, enums validated against allowlists.
- WAFs — defence-in-depth, not a substitute for parameterised queries.
Under the Hood
Live demonstration using Python’s sqlite3 — the difference between vulnerable and parameterised:
import sqlite3
conn = sqlite3.connect(':memory:')
conn.execute("CREATE TABLE users (id INT, name TEXT, password TEXT)")
conn.execute("INSERT INTO users VALUES (1,'alice','s3cret')")
conn.execute("INSERT INTO users VALUES (2,'bob','hunter2')")
conn.commit()
def login_vulnerable(name, pw):
q = f"SELECT * FROM users WHERE name='{name}' AND password='{pw}'"
return conn.execute(q).fetchone()
def login_safe(name, pw):
return conn.execute(
"SELECT * FROM users WHERE name=? AND password=?", (name, pw)
).fetchone()
print("=== VULNERABLE ===")
print("normal login:", login_vulnerable("alice", "s3cret"))
injection = "' OR 1=1 --"
print("injected :", login_vulnerable(injection, "anything")) # bypasses auth!
print()
print("=== PARAMETERISED (safe) ===")
print("normal login:", login_safe("alice", "s3cret"))
print("injected :", login_safe(injection, "anything")) # returns None
The parameterised version treats the injection string as a literal search term — the DB looks for a user literally named ' OR 1=1 --, finds none, and returns nothing.
Engineering Trade-offs
- Parameterised queries vs ORMs. ORMs use parameterised queries internally and add a layer of abstraction — but they offer raw-query escape hatches (
execute(),raw()) that bypass safety. Safety requires discipline, not just tool choice. - Parameterised queries vs stored procedures. Stored procedures are safe if they don’t dynamically build queries internally; they add DB coupling and migration complexity with no security benefit over parameterised queries in application code.
- WAF as a supplement. WAFs can block known injection patterns but are bypassable by obfuscation, and cannot distinguish malicious input from bizarre-but-legitimate queries. They reduce risk without eliminating it.
- Least privilege reduces blast radius. Even with injection, a DB user that can only
SELECTfrom the app’s tables limits the attacker far more than theSUPERUSERrole most defaults use. Scope the DB account to exactly what the app needs.
Real-world examples
- The 2008 Heartland Payment Systems breach (130 million card numbers) started with SQL injection.
- The 2011 Sony Pictures hack (LulzSec) was SQL injection; the published dump went on for pages.
- TalkTalk’s 2015 breach (157,000 customer records, £77M fine) was SQL injection through a legacy page the company had forgotten existed.
- Bobby Tables is the universal mnemonic:
Robert'); DROP TABLE Students;--
Common misconceptions
- “My ORM protects me.” Yes — as long as you only use parameterised queries through it. Raw queries with string interpolation bypass safety. Most ORMs have escape hatches.
- “SQL injection is a solved problem.” The fix is well-known. The bugs are still shipped every year, usually in legacy code or “quick” raw queries added during debugging.
Try it yourself
See injection bypass and parameterised defence side-by-side in an in-memory database:
python3 -c "
import sqlite3
conn = sqlite3.connect(':memory:')
conn.execute('CREATE TABLE users (name TEXT, pw TEXT)')
conn.execute(\"INSERT INTO users VALUES ('alice','s3cret')\")
conn.commit()
injection = \"' OR 1=1 --\"
# Vulnerable
row = conn.execute(f\"SELECT * FROM users WHERE name='{injection}' AND pw='x'\").fetchone()
print('vulnerable — got row:', row)
# Safe
row2 = conn.execute('SELECT * FROM users WHERE name=? AND pw=?', (injection, 'x')).fetchone()
print('parameterised — got row:', row2)
"
Learn next
- XSS — the other half of the classic web injection pair.
- Vulnerability — how flaws like this are tracked, scored, and disclosed via CVE/CVSS.
- Threat model — the discipline for deciding which injection surfaces in your app are highest priority.
Read this in a learning path
All paths →This topic is part of a learning path. Start in context to keep prev/next and progress tracking.
Relationships
- Requires
- Related
- Next
- Leads to
Neighborhood
A visual companion to the relationships above. Click any node to visit that topic.