SQL Injection (SQLi)
The Database Destroyer
SQL Injection (SQLi) is a devastating vulnerability that occurs when an application improperly sanitizes user input before inserting it into a database query. This allows an attacker to manipulate the SQL statement, tricking the database into executing malicious commands.
The impact ranges from bypassing authentication to dumping the entire database, or even gaining remote code execution on the database server.
Anatomy of a SQL Injection
Imagine a vulnerable Python backend checking a username and password:
# VULNERABLE CODE - NEVER DO THIS
query = f"SELECT * FROM users WHERE username = '{user_input}' AND password = '{password_input}'"
db.execute(query)
If the attacker enters admin as the username, and the following string as the password:
' OR '1'='1
The resulting query becomes:
SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'
Because '1'='1' is always mathematically true, the database ignores the password check entirely and logs the attacker in as the admin.
Advanced Injection Types
- UNION-Based SQLi: The attacker uses the
UNIONoperator to append results from a completely different table (like thepasswordstable) into the application's response. - Blind SQLi (Boolean): The application doesn't return the database output, but it behaves differently (e.g., HTTP 200 vs 404) if a query is true or false. The attacker asks the database yes/no questions ("Is the first letter of the admin password 'a'?") and infers the answer based on the HTTP response.
- Time-Based Blind SQLi: When the application returns the exact same response regardless of true/false, the attacker injects
SLEEP(10). If the response takes 10 seconds, the attacker knows their injected condition was true.
The Absolute Defense: Parameterized Queries
The only acceptable way to prevent SQL Injection is by using Parameterized Queries (Prepared Statements).
Instead of concatenating strings, you send the SQL query template and the data separately to the database driver. The database compiles the SQL logic first, and then treats the user input strictly as literal string data, not executable code.
# SECURE CODE - Parameterized Query
query = "SELECT * FROM users WHERE username = ? AND password = ?"
# The database driver handles the variables safely
db.execute(query, (user_input, password_input))
ORMs (Object-Relational Mappers) like Django ORM, SQLAlchemy, or Prisma use parameterized queries by default, heavily mitigating SQLi risks right out of the box. However, if you write raw SQL or use .raw() methods in an ORM, you must still implement parameters manually.
Beyond the WHERE Clause
SQL injection is not just a login-bypass trick; it strikes anywhere user data lands inside SQL text. The UNION technique appends rows from unrelated tables (usernames, credit cards) directly into the result set the application renders. Blind techniques work even when the output is never shown: boolean-based injection measures whether a query variant returns results (HTTP 200 vs 404), and time-based injection uses SLEEP() to measure truth ("if this condition is true, the query waits ten seconds"). Stacked queries (SELECT ...; DROP TABLE ...) extend injection to arbitrary statements when the driver supports multiple statements per call — the single most damaging vector for a database with an application account that has DELETE or DROP rights.
A List, NOT an Element
Identifiers cannot be parameterized the same way values can — a table name, a column name, or an ORDER BY column cannot be bound as a parameter on most drivers. When user input must select an identifier, put it through an allowlist of known-good values, never interpolate the literal string:
ALLOWED_COLUMNS = {"name", "price", "created_at"}
col = ALLOWED_COLUMNS.get(user_choice) # None -> safe default
if col is None:
raise ValueError("unsupported sort column")
query = f"SELECT * FROM products ORDER BY {col} ASC"
This avoids the second-most common injection class: developer-supplied query fragments that were "safe in my tests" but compose unsafely when paired with a % or ' the attacker controls.
Defense in Depth for Databases
Parameterization eliminates the injection vector, and three more layers make the blast radius manageable. Least privilege: give the application database account only the exact privileges it needs (a SELECT-only replica for read paths, no DROP/CREATE to service accounts). Input validation: reject obviously malformed inputs early, even though it is secondary. Defense at the edge: a WAF (firewalls lesson) can block obvious ' OR 1=1 patterns, and database-level audits log what fuzzers probe. Add runtime monitoring (unexpected UNION or SLEEP tokens reaching your DB) and you will catch the attempt even if past code ever injected — because the past keeps writing queries, and every string-built query is a candidate.