This SQL injection cheatsheet gives you the short version first: use parameterized queries for every statement, never concatenate user input into SQL, apply least-privilege database accounts, and validate input types at the edge. Everything else is detail on those four rules. If your code follows them consistently, the classic login-bypass, UNION, and blind injection attacks all stop working, because the database stops treating user input as executable SQL.
The rest of this page turns that summary into copy-ready patterns and the specific anti-patterns to ban in code review.
What is the one rule that prevents SQL injection?
Parameterized queries. When you bind input as a parameter, the database driver sends the query structure and the data separately, so input can never change the statement's meaning. This is the definitive answer to how to prevent sql injection, and it holds across every language:
# Python (psycopg2)
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
// Java (JDBC)
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE email = ?");
ps.setString(1, email);
// Node.js (pg)
client.query("SELECT * FROM users WHERE email = $1", [email]);
Notice what is missing: any string building. The placeholder (%s, ?, $1) is not string interpolation; it is a binding the driver handles. If you ever see a query assembled with +, template literals, or format(), that line is the bug.
How should you use an ORM safely?
ORMs are safe on their parameterized paths and dangerous the moment you drop to raw SQL. The cheatsheet rule: prefer the query builder, and treat every raw-SQL method as review-required.
Safe:
// Sequelize, parameterized under the hood
User.findAll({ where: { status: userStatus } });
Dangerous (bans this in review):
// Raw string interpolation defeats the ORM
db.query(`SELECT * FROM users WHERE status = '${userStatus}'`);
The same applies to Django's .raw() and .extra(), Hibernate dynamic HQL, and Entity Framework's FromSqlRaw. When you must use raw SQL, pass parameters through the method's binding arguments, never through the format string.
What input validation belongs in the cheatsheet?
Validation is defense in depth, not a replacement for parameterization. Add it because it shrinks the attack surface and catches malformed input early:
- Type-check known shapes. An
idshould parse as an integer; reject anything that does not. A numeric cast alone stops a large share of injection attempts against numeric parameters. - Allowlist enumerated values. Sort direction, column names for
ORDER BY, and status filters cannot be parameterized in most drivers, so validate them against a fixed allowlist rather than interpolating them. - Constrain length and character set where the domain is known (a country code, a currency).
- Never trust "already stored" data. Second-order injection reuses stored values in a later query. Treat data read from your own database as untrusted when it flows into a new query.
The ORDER BY column case deserves emphasis: since you cannot bind an identifier, map user input to a known-safe column name in code ({ "date": "created_at" }) instead of passing it through.
What are the supporting controls?
Parameterization stops the injection; these controls limit the blast radius if something slips through:
- Least privilege. The application's database account should have only the permissions it needs. A read API account that cannot
DROP,UPDATE, or read the credentials table turns a catastrophic breach into a contained one. - Separate accounts per service. Do not share one superuser connection string across every microservice.
- Disable verbose SQL errors in production. Detailed database errors hand attackers the schema and confirm injectability. Return a generic error and log the detail server-side.
- Patch drivers and ORMs. Injection-adjacent CVEs appear in database libraries; keep them current, and use an SCA scan so you know which versions are actually in your build.
- Add a WAF as one shallow layer. It blocks lazy scanners but is trivially bypassed with encoding, so never treat it as the fix.
What is the review checklist?
Print this and paste it into your pull-request template. Reject any diff where:
- User input reaches a query via
+, template literals,%,.format(), orf-strings. - A raw-SQL ORM method (
.raw(),FromSqlRaw,createNativeQuery) receives interpolated input. ORDER BY,LIMIT, or a table/column name comes from a request without an allowlist mapping.- Stored data is concatenated into a new query without being treated as untrusted.
- A database account has broader privileges than the endpoint requires.
Automating this checklist is where tooling helps. Static analysis traces tainted input from request handlers to query sinks and flags exactly the concatenation sites this list bans, while a dynamic probe confirms the payload no longer executes. Combining SAST and DAST covers both the source-level pattern and the running behavior. Safeguard's static engine reports each unsafe query construction as a specific file and line, so the review checklist becomes an automated gate instead of a manual read-through.
FAQ
What is the single most important item on a SQL injection cheatsheet?
Parameterized queries. Binding input as a parameter rather than concatenating it into the SQL string means the database never interprets user data as code. Every other control is defense in depth; this one is the actual fix.
How do I prevent SQL injection when the ORM cannot parameterize a column name?
Use an allowlist. You cannot bind identifiers like column or table names in most drivers, so map user input to a fixed set of known-safe values in your code (for example, translate "newest" to created_at). Never interpolate the raw identifier into the query.
Does escaping input prevent SQL injection?
Escaping is fragile and not recommended as a primary defense. Character-escaping rules differ by database and encoding, and attackers routinely find bypasses. Parameterized queries remove the need to escape at all, which is why they are the recommended approach.
Can a WAF replace secure coding for SQL injection?
No. A web application firewall blocks obvious payloads and lazy scanners, but attackers bypass signatures with encoding, comments, and case tricks. Treat a WAF as one thin layer on top of parameterized queries, never as a substitute for fixing the code.