Handbooks / Web Security (OWASP) / Chapter 1

Injection

7 pages · ~5 min✓ Reviewed

Start here. Next up: Cross-Site Scripting (XSS).

Web Security (OWASP)Page 1

Injection Mental Model

1. What Injection Is

  • Injection = untrusted input crossing a trust boundary, run as code by an interpreter
  • SQL engine, shell, filesystem, HTTP client
  • Root cause: data channel and code channel mixed in one string
  • Fix: keep them separate, not clean the string

2. Source to Sink

  1. Sourcequery, header, cookie, JSON, filename
  2. Boundaryuntrusted text in a string
  3. Sinkquery exec, shell, file open, fetch

3. Example

  • The quote ends the string; OR '1'='1 is always true
  • Result: every row comes back
python
name = "' OR '1'='1"
print("SELECT * FROM users WHERE name='" + name + "'")

input became SQL structure

output
SELECT * FROM users WHERE name='' OR '1'='1'
Remember

Never let data become code. Separate beats sanitize.

  • Escaping vs parameters? Escaping patches one string; parameters never mix data into code
  • Sink? The call that executes input: query exec, shell, file open, fetch
  • Client-side validation? Attackers skip the browser and call the API directly
Web Security (OWASP)Page 2

SQL Injection and Parameterized Queries

1. Parameterized Queries

  • Template and values are sent separately
  • Values stay data, never query structure
  • Python %s; JDBC PreparedStatement with ?
  • Unsafe: f-string, +, String.format in SQL
python
cur.execute("SELECT * FROM users WHERE id=%s", (uid,))
cur.execute(f"SELECT * FROM users WHERE id={uid}")

first is safe, second is injectable

2. Attack Types

TypeData leavesExamples
In-bandin the responseUNION, error-based
Blindinferred from true/false or delayboolean, time-based
Out-of-bandsent via DNS or HTTPexfiltration

3. Identifiers

  • Placeholders bind values only
  • Not table names, column names or ORDER BY direction
  • Allowlist those identifiers in code instead
python
SORT = {"name": "name", "new": "created_at"}
col = SORT.get(by, "name")
cur.execute(f"SELECT * FROM users ORDER BY {col}")
Web Security (OWASP)Page 3

ORM Pitfalls and Command Injection

1. ORM Escape Hatches

  • ORMs parameterize by default
  • Raw escape hatches reopen injection
  • Risk = the string you build, not the library
ORMUnsafe call
Django.raw(), .extra()
SQLAlchemytext(f"...")
Sequelizequery with interpolation
Railswhere("name='#{x}'")
python
q = text("SELECT * FROM users WHERE id=:id")
conn.execute(q, {"id": uid})

safe: bind params

2. Operator Injection

  • Body passed straight into a filter
  • NoSQL: attacker sends {"$ne": null}, matches anything
  • Mass assignment: body sets is_admin=true
  • Fix: allowed fields only

3. Command Injection

  • Input in a shell: ; | && $() backticks add commands
  • ping 8.8.8.8; rm -rf / runs both
  • Bug: shell=True or os.system with input
  • Fix: argument array, shell=False, allowlist and validate args
python
import subprocess
host = "8.8.8.8"
r = subprocess.run(["ping", "-c", "1", host],
                   shell=False, capture_output=True)

host stays one argument

Web Security (OWASP)Page 4

Path Traversal, SSRF and Input Validation

1. Path traversal

  • ../, %2e%2e%2f, absolute paths, symlinks escape the base dir
  • e.g. /download?file=../../etc/passwd
  • Blocking ../ alone is bypassable
  • Fix: realpath, then check it starts with BASE; or map IDs to filenames
python
import os
from urllib.parse import unquote
BASE = os.path.realpath("/srv/files")
name = unquote("%2e%2e/%2e%2e/etc/passwd")
p = os.path.realpath(os.path.join(BASE, name))
print(p.startswith(BASE + os.sep))

decode first, resolve, then compare with the resolved BASE

output
False

2. SSRF

  • Server fetches a user-supplied URL, so attackers reach internal services
  • Cloud metadata http://169.254.169.254/ leaks instance credentials
  • Allowlist hosts
  • Resolve DNS, block private, loopback, link-local IPs
  • Re-validate every redirect (stops 302-to-internal)
  • Pin the validated IP (stops DNS rebinding)
  • Disable file://, gopher:// schemes
  • Use IMDSv2 on AWS

3. Input validation

  • Allowlist (type, length, format, range) beats blocklist
  • Validate server-side
  • Canonicalize (decode) before checking
Defense in depth

Validation never replaces parameterization or output encoding.

Web Security (OWASP)Page 5

Common Mistakes and Fixes

1. Four mistakes, four fixes

MistakeFix
Escaping or quote-stripping, not parametersBind parameters everywhere
Concatenated ORDER BY, table or column namesMap input to a fixed allowlist
Client-side only, or checked before decodingServer-side check after canonicalization
DB account can DROP or read all schemasLeast privilege, read-only users, generic errors, no SQL or stack traces
python
SORTS = {"name": "name", "new": "created_at"}
col = SORTS.get(sort, "created_at")
cur.execute(f"SELECT * FROM t WHERE id=%s ORDER BY {col}", (uid,))

values bound, identifier picked from the allowlist

2. Defenses and review

  1. Parameterizestrongest
  2. Allowlistvalidate
  3. Least privilegelimit damage
  4. Safe errorsno leaks
  • Review: hunt string-built SQL, shell=True, os.system, raw ORM calls fed by input
  • SAST flags source-to-sink flows; DAST and fuzzing send payloads at live endpoints
  • Logs: alert on SQL errors, 500 spikes, blocked ../ paths
Remember

Escaping cleans the string; parameters keep data out of the code channel.

Web Security (OWASP)Page 6

Injection Defenses Cheat Sheet

1. Attack, Sink, Fix

AttackSinkPrimary fix
SQLiquery execbind parameters
Command injectionshellargument array, shell=False
Path traversalfile openrealpath + base-dir check
SSRFoutbound fetchhost allowlist + private-IP block

2. Safe vs Unsafe

python
import os, subprocess
cur.execute("... WHERE id=%s", (uid,))  # safe
cur.execute(f"... WHERE id={uid}")  # unsafe
subprocess.run(["ping", "-c", "1", host])  # safe
os.system(f"ping {host}")  # unsafe

fragment: cur, uid and host come from the request

3. Payload Recognizers

PayloadAttack
' OR 1=1--SQLi
; idcommand injection
../../etc/passwdpath traversal
http://169.254.169.254/latest/meta-data/SSRF
  • SSRF: re-validate every redirect target
  • SSRF: pin the validated IP to stop DNS rebinding

4. Layers in Order

  1. Parameterize
  2. Allowlist
  3. Least privilege
  4. Safe errors
  5. Monitoring
One rule

Never let data become code. Keep data and code in separate channels.

Web Security (OWASP)Page 7

Check yourself

cur.execute('SELECT * FROM users ORDER BY %s', (col,)) runs with col = 'age'. Does it sort by the age column?
  • No: the value is bound as the string literal 'age', so ORDER BY gets a constant, not a column
  • MySQL silently sorts by that constant; PostgreSQL rejects it: non-integer constant in ORDER BY
  • Either way the column is unused. Fix: allowlist column names in code
What happens with subprocess.run(f'ping -c 1 {host}', shell=True) when host is 8.8.8.8; id?
  • The shell splits at ; and runs ping, then id
  • Fix: subprocess.run(['ping','-c','1',host]) with shell=False
A filter does name.replace('../',''). What path comes out for ....//etc/passwd?
  • ../etc/passwd: removing the inner ../ leaves a new one
  • Fix: realpath, then check it starts with the base directory