Description
SQL injection occurs when user-supplied input is incorporated into a database query without proper parameterisation or escaping. The database engine cannot distinguish between the intended query structure and the attacker-supplied SQL, so it executes both.
Despite being one of the oldest and most documented vulnerability classes, SQL injection remains consistently present in production applications — particularly in legacy codebases, third-party components, and less-tested administrative interfaces.
How It Works
Consider a login query constructed by string concatenation:
SELECT * FROM users WHERE username = '$input' AND password = '$pass'
An attacker supplies ' OR '1'='1' -- as the username. The resulting query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1' --' AND password = '...'
The -- comments out the password check. The OR '1'='1' always evaluates to true. The attacker is authenticated as the first user in the table — typically an administrator.
Beyond authentication bypass, SQL injection enables:
- UNION-based extraction — append a
UNION SELECTto read from arbitrary tables. - Blind (boolean/time-based) injection — infer data character by character through conditional responses or deliberate delays (
SLEEP(),WAITFOR DELAY). - Out-of-band exfiltration — in some database configurations, trigger DNS lookups to exfiltrate data without visible query output.
Impact
- Full database read — every table, including credentials, PII, payment data, and internal configuration.
- Authentication bypass — log in as any user, including administrators.
- Data modification and deletion —
UPDATEorDROParbitrary tables. - Remote code execution — on SQL Server via
xp_cmdshell; on MySQL viaINTO OUTFILEto write web shells; on PostgreSQL viaCOPY TO/FROM PROGRAM. - Lateral movement — use database credentials to access other internal services.
Detection
A systematic approach covers more ground than ad-hoc testing:
- Inject a single quote into every parameter. Unexpected errors, changed responses, or application crashes indicate reflection of input into a query.
- Boolean tests — inject
' AND 1=1 --(true) and' AND 1=2 --(false). Differing responses confirm injection. - Time-based blind tests — inject
'; WAITFOR DELAY '0:0:5' --(SQL Server) or'; SELECT SLEEP(5) --(MySQL). A five-second delay confirms execution. - Second-order injection — data stored cleanly is later used unsafely in another query. Check update flows, admin panels, and report generation separately.
Remediation
Parameterised queries (prepared statements) are the only reliable fix. Never concatenate or interpolate user input into SQL strings.
# Vulnerable
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# Safe
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
If parameterised queries are not feasible in a specific context (e.g., dynamic table names), use a strict allowlist — never a blocklist.
Least-privilege database accounts. The application database user should have only the permissions it needs — typically SELECT, INSERT, UPDATE on specific tables. It should never have DROP, CREATE, or file system access.
Web Application Firewall (WAF). A WAF provides defence-in-depth but is not a substitute for parameterisation. Bypass techniques exist for every WAF in production.
