04. Injection
Injection happens when your input stops being data and starts being code.
SQL Injection
Apps store data in databases and query them with SQL. The bug appears when the app builds queries by gluing strings together:
# vulnerable
query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"
Normal input alice / hunter2 produces:
SELECT * FROM users WHERE username = 'alice' AND password = 'hunter2'
But what if the username is admin'--?
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'
The ' closes the string, and -- starts a comment, so the password check disappears. You're logged in as admin.
Step 1: Detect it
| Input | What to look for |
|---|---|
' | A database error, or the page breaks |
' OR '1'='1 | More results than expected |
' AND '1'='2 | No results |
Step 2: Extract data with UNION
UNION glues the results of a second query onto the first. Both queries must return the same number of columns.
-- 1. Find the column count: increase until it errors
' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--
' ORDER BY 4-- ← error, so there are 3 columns
-- 2. Confirm with UNION
' UNION SELECT 'a','b','c'--
-- 3. List tables (SQLite)
' UNION SELECT name, sql, 1 FROM sqlite_master--
-- 4. Dump what you want
' UNION SELECT username, password, 1 FROM users--
sqlite_master · MySQL / PostgreSQL: information_schema.tables and information_schema.columns
The fix
# parameterized query: input is always treated as data
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))
Command Injection
Same idea, different interpreter. The app passes your input into a shell command:
# vulnerable
os.system("ping -c 2 " + host)
Shells let you chain commands. If host is 8.8.8.8; whoami, the server runs both.
| Operator | Behavior |
|---|---|
; | Run the next command regardless |
&& | Run the next only if the first succeeded |
|| | Run the next only if the first failed |
| | Pipe output of the first into the second |
$(cmd) or `cmd` | Run cmd and substitute its output |
8.8.8.8; id
8.8.8.8 && ls -la
x || cat /etc/passwd
The fix
Don't call a shell with user input. Use a library function, or pass arguments as a list (subprocess.run(["ping", "-c", "2", host])) and validate the input against an allowlist.