Skip to main content

All weeks · Worksheet · Overview

Week 4 · Lecture slides

Week 4

Contents23 sections

Injection & Input Handling

Software Security · Nutthakorn Chalaemwongwan


Today

  • Why injection is still #1-class of bug
  • The general injection pattern
  • SQL injection — hands-on
  • Command injection
  • Defenses: parameterized queries + validation
  • 🎮 Game: SQLi Warm-up

Recap — Week 3

  • Crypto failures: ECB, weak hashes, hardcoded keys
  • Hashing ≠ encryption ≠ encoding
  • Use vetted KDFs (bcrypt/argon2) + authenticated encryption

The injection pattern (one idea)

Untrusted data gets interpreted as code/commands.

  • Attacker input crosses a trust boundary into an interpreter
  • SQL, OS shell, LDAP, XPath, template engines, NoSQL…
  • Maps to OWASP A05:2025 Injection

SQL injection — how it works

-- vulnerable (this week's app.py, SQLite)
"SELECT * FROM users WHERE username = '" + input + "'"
  • Input ' OR '1'='1 → always true
  • UNION-based → read other tables in one shot (today's lab)
  • Stacked queries ('; DROP TABLE users; --) are the textbook example, but don't assume they work everywhere — Python's sqlite3 refuses multiple statements in one execute() call; this week's target raises an error instead

Try it — watch the parse tree change

Type an input. See exactly where the quote breaks out of the string.

How concatenation changes the SQL parse tree (Week 4) — open full size

Auth bypass — anatomy of one payload

Inject into the username field:

alice'--
SELECT * FROM users WHERE username = 'alice'--' AND password = '...';
  • ' closes the string literal you're injected into
  • -- comments out everything after it, including the password check
  • No trailing space, no LIMIT trick needed — this app is SQLite via fetchone(), not a MySQL row-count gate; don't carry over MySQL-specific folklore ("needs a space after --") — that's a different engine's quirk

The fix: parameterize — data stays data

# concatenation: DB gets ONE finished string, decides grammar AFTER
"...WHERE username = '" + user + "'"     # user = alice'--  ->  ' breaks out, code

# parameterized: structure parsed and LOCKED first, value bound AFTER
db.execute("...WHERE username = ? AND password = ?", (user, pw))
  • ? locks the query structure before the input ever arrives
  • alice'-- is then bound as a value — it can no longer become SQL grammar
  • Escaping quotes patches symptoms; this is the actual fix for SQLi

SQLi in one picture — break-out vs. bound value

Three SQL login queries stacked: normal input sits inside the string as data; the injected username alice'-- makes the user's quote close the string and -- comment out the password check so it logs in with no password; and the parameterised version binds alice'-- into a ? placeholder as pure data so the quote and dashes are inert and login fails.


Real-world impact

  • Equifax (2017): unvalidated HTTP header → Struts RCE (CVE-2017-5638); 147M people, ~$700M settlement
  • Heartland (2008): SQLi into payment systems → 130M+ cards
  • Bulgaria: data on almost all adults leaked via SQLi
  • Impact: data leak/modification, full DB control, DoS, reputation

Validate: allow-list, not block-list

  • Allow-list (preferred): accept only known-good (e.g. 0–9 for a phone)
  • Block-list: ban known-bad → bypassed by new payloads
  • Validate type · length · range · format
  • Client-side for UX, server-side for security

Demo / attack surface

  • Login forms, search boxes, URL params, JSON fields, headers
  • Error messages leak schema → enumeration
  • Today's lab: raw payloads via browser/curl against app.py — no scanner needed to see the mechanism
  • Burp Suite / sqlmap are real-world tools worth knowing (sandbox-only, ethically) — not required for this week's tasks

Command injection

system("ping -c 1 " . $_GET['host']);   // vulnerable
// host = 8.8.8.8; cat /etc/passwd
  • Shell metacharacters: ; | & $() \ >`
  • Leads to RCE — full server compromise

The fix: no shell — pass an argument vector

# vulnerable: shell=True -> the OS shell parses the whole string
subprocess.run("ping -c 1 " + host, shell=True)     # host = 8.8.8.8;cat /flag.txt

# fixed: shell=False + argument array -> no shell, no metacharacters
subprocess.run(["ping", "-c", "1", host], shell=False)   # + allow-list host
  • shell=True hands your string to /bin/sh, which reads ; | $() as syntax
  • An arg vector runs ping directly — host is one literal argument, never parsed
  • 8.8.8.8;cat /flag.txt becomes one impossible "hostname"; the ; is inert

Upload → RCE (a classic chain)

The classic chain, attacker uploads shell.php:

<?php system($_GET['cmd']); ?>
  • If the upload dir is web-served and PHP-executable: .../uploads/shell.php?cmd=ls%20-l → RCE
  • This week's lab stops one step earlier: CWE-434 unrestricted upload — the app accepts any file type/extension, but the upload directory is not served or executed. Your task: explain which missing control would complete the chain (type check, extension allow-list, storage outside web root, no-exec) — not demonstrate a live shell
  • Fix: validate file type via mime_content_type(), allow-list extensions, store uploads outside web root, no execute

CWE mapping

  • CWE-89 — SQL injection
  • CWE-78 — OS command injection (CWE-77 is its general parent — cite CWE-78, the specific one)
  • CWE-434 — unrestricted upload of a dangerous file type

Defenses that actually work

  • Parameterized queries / prepared statements (the fix for SQLi)
  • ORM with bound parameters
  • Allow-list input validation (type, length, format)
  • Avoid shells: use exec APIs with arg arrays, not string concat
  • Least-privilege DB accounts

One value, three interpreters

One untrusted request value reaches three different interpreters — SQL (CWE-89, fixed by a parameterized query), the OS shell (CWE-78, fixed by an argument vector with shell=False), and a filesystem write (CWE-434, fixed by an extension allow-list). One input filter cannot guard three grammars — the fix belongs at the sink that parses the value, not at the source.

  • SQL (WHERE username = <in>) → parameterized query — bind the value
  • Shell (ping … <in>) → argument vector + shell=False — no shell to parse it
  • Filesystem (save(<in>)) → extension allow-list + store outside web root
  • One filter can't guard three grammars — fix at each sink, not at the source

⚔️ Game — SQLi Warm-up

Four hits against this week's own app — no filters to bypass, the app has none:

  1. Auth bypass (alice'--)
  2. UNION dump (steal all credentials)
  3. Command injection
  4. Unrestricted upload
  5. Warm-up cleared: run solution_app.py, prove all four attacks now fail, cite the exact fix line for each

Lab steps

📋 Worksheet 4 — labs/week04-injection/worksheet.md (Part 3) · kickoff: docker compose up → http://localhost:8080

docker run --rm -p 80:80 vulnerables/web-dvwa   # optional extra target (or Juice Shop)
  1. Find an injectable parameter, bypass auth
  2. Extract data (UNION dump)
  3. Achieve command injection
  4. Probe the upload endpoint — document the missing control
  5. Rewrite the endpoints safely & re-test

Deliverable

  • Findings: each injection point + payload + impact (CWE-mapped)
  • The fixed code (prepared statements / validation)
  • Proof the payload no longer works
  • + Audit the AI / EiPE / Prompt Problem (see worksheet)

Key takeaways

  • Never build interpreter strings from untrusted input
  • Parameterize first; validate as defense-in-depth
  • Injection = data treated as code

Questions?

Next week: XSS & client-side risks

All weeks in Software Security