SQL injection is one of the oldest and most damaging web application vulnerabilities, and it happens whenever untrusted input — something a user typed into a form — gets inserted directly into a SQL query string instead of being treated as pure data. If an attacker can control part of your query's actual syntax, they can potentially read data they shouldn't, modify or delete records, or in the worst cases take over the entire database.
The Vulnerable Pattern: String Concatenation
Here's a login check built by concatenating user input directly into a query string — this is exactly what not to do:
# VULNERABLE — never do this
username = request.form["username"]
password = request.form["password"]
query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"
cursor.execute(query)
If a user types a normal username, this looks harmless. But nothing stops an attacker from typing something like ' OR '1'='1 into the username field instead. Once concatenated, the query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = ''
Since '1'='1' is always true, this condition can match every row in the table, potentially logging the attacker in as the very first user it finds — no valid password required. More aggressive payloads can go further, chaining in a DROP TABLE or extracting data from entirely unrelated tables.
The Fix: Parameterized Queries
The fix isn't "sanitize the input more carefully" — it's to stop building queries out of string concatenation entirely. Parameterized queries (also called prepared statements) send the SQL structure and the user-supplied values to the database separately, so the database always knows which parts are code and which parts are just data, no matter what characters the input contains:
# SAFE — username and password are passed as parameters, never concatenated
query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))
With this version, even if someone enters ' OR '1'='1 as the username, the database treats the entire string as a literal value to search for — not as SQL syntax — so the query correctly finds no matching user. Every major language's database library supports parameterized queries: ? or %s placeholders are standard in Python, PHP's PDO uses named or positional placeholders, and Java's PreparedStatement works the same way.
Other Layers of Defense
ORMs (Object-Relational Mappers) like SQLAlchemy, Django's ORM, or Prisma generate parameterized queries for you automatically under the hood, which is one of the quieter security benefits of using one — as long as you stick to the ORM's query-building API and avoid dropping into raw string-built SQL yourself. Combine this with the principle of least privilege (your application's database user shouldn't have permission to drop tables if it only ever needs to SELECT and INSERT data) and basic input validation as a defense-in-depth measure — but parameterized queries are the one fix that actually closes the vulnerability, not just a mitigation.
