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

InputWhat to look for
'A database error, or the page breaks
' OR '1'='1More results than expected
' AND '1'='2No 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--
Where tables are listed SQLite: 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.

OperatorBehavior
;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.

05-xss →