All Cheat Sheets
SQL Injection Prevention Cheat Sheet
SQL injection prevention cheat sheet: vulnerable vs safe parameterized queries in Node, Python, Java, PHP, C# and ORMs, identifier allow-lists, least privilege, and OWASP guidance.
What SQL Injection Is
DefinitionSQL injection (SQLi) happens when untrusted input is concatenated into a SQL string, so the database parses part of the input as SQL code instead of data.
ImpactReading or changing data the user should not see, bypassing login checks, deleting tables, and in some configurations running commands on the database host.
Root causeMixing code and data in one string. The fix is to keep them separate: send the query text and the values to the driver independently.
Where input comes fromForm fields, URL query strings, path segments, JSON bodies, headers, cookies, file contents, and data previously stored in your own database (second-order injection).
Primary defenseParameterized queries (prepared statements) for every value. Validation and escaping are supporting layers, not replacements.
Vulnerable pattern (any language)
"SELECT * FROM users WHERE email = '" + email + "'"String concatenation, template literals, f-strings, format() and sprintf all have this flawSafe pattern (any language)
"SELECT * FROM users WHERE email = ?" + [email]Placeholder in the SQL, value passed separately; the driver never parses the value as SQLNode.js (pg, mysql2)
Vulnerable: pg
await client.query(
`SELECT * FROM users WHERE email = '${email}'`
);A template literal is still string concatenationSafe: pg
await client.query(
'SELECT * FROM users WHERE email = $1',
[email]
);PostgreSQL uses numbered placeholders $1, $2, ...Safe: pg IN list
await client.query(
'SELECT * FROM users WHERE id = ANY($1)',
[ids] // JS array -> Postgres array
);Vulnerable: mysql2
conn.query("SELECT * FROM users WHERE id = " + req.params.id);Safe: mysql2 execute
const [rows] = await conn.execute(
'SELECT * FROM users WHERE email = ? AND active = ?',
[email, true]
);execute() uses a server-side prepared statementmysql2 query() with ?
await conn.query('SELECT * FROM t WHERE id = ?', [id]);Safe for values, but escaped client-side; prefer execute()Python (psycopg, sqlite3)
Vulnerable: f-string
cur.execute(f"SELECT * FROM users WHERE email = '{email}'")Safe: psycopg 3
cur.execute(
"SELECT * FROM users WHERE email = %s",
(email,),
)%s is a placeholder here, not Python string formatting; note the 1-tupleSafe: named params
cur.execute(
"SELECT * FROM users WHERE email = %(email)s",
{"email": email},
)Never do this
cur.execute("... WHERE email = '%s'" % email)The % operator formats before the driver sees the querySafe: sqlite3
cur.execute("SELECT * FROM users WHERE email = ?", (email,))
cur.execute("... WHERE email = :email", {"email": email})Identifiers: psycopg.sql
from psycopg import sql
q = sql.SQL("SELECT * FROM {} WHERE id = %s").format(
sql.Identifier(table)
)
cur.execute(q, (row_id,))Quotes the identifier correctly; still allow-list the table nameJava (JDBC, JPA, Spring)
Vulnerable: Statement
Statement st = conn.createStatement();
st.executeQuery("SELECT * FROM users WHERE email = '" + email + "'");Safe: PreparedStatement
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE email = ?");
ps.setString(1, email);
ResultSet rs = ps.executeQuery();Parameter indexes start at 1Safe: JPA / JPQL
em.createQuery(
"SELECT u FROM User u WHERE u.email = :email", User.class)
.setParameter("email", email)
.getResultList();Vulnerable: JPQL concat
em.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'")JPQL and HQL injection are real; ORMs do not save you from concatenationSafe: Spring JdbcTemplate
jdbcTemplate.query(
"SELECT * FROM users WHERE email = ?",
userRowMapper, email);Spring Data
@Query("SELECT u FROM User u WHERE u.email = :email")
User findByEmail(@Param("email") String email);Derived methods like findByEmail(...) are parameterized automaticallyPHP (PDO, mysqli)
Vulnerable
$pdo->query("SELECT * FROM users WHERE email = '$email'");Variable interpolation in double quotes is concatenationSafe: PDO named
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);
$user = $stmt->fetch();Safe: PDO positional
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ? AND org = ?');
$stmt->execute([$id, $orgId]);Real prepares
$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);Use native prepared statements and throw on errorsSafe: mysqli
$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?');
$stmt->bind_param('s', $email);
$stmt->execute();'s' = string, 'i' = integerC# / .NET (ADO.NET, Dapper, EF Core)
Vulnerable
var cmd = new SqlCommand(
"SELECT * FROM Users WHERE Email = '" + email + "'", conn);Safe: SqlCommand parameters
var cmd = new SqlCommand(
"SELECT * FROM Users WHERE Email = @Email", conn);
cmd.Parameters.Add("@Email", SqlDbType.NVarChar, 256).Value = email;Explicit type and size give better plans than AddWithValueSafe: Dapper
var user = conn.QuerySingleOrDefault<User>(
"SELECT * FROM Users WHERE Email = @Email",
new { Email = email });Safe: EF Core FromSql
ctx.Users.FromSql($"SELECT * FROM Users WHERE Email = {email}")Interpolated values become DbParameters (EF Core 7+; FromSqlInterpolated earlier)Vulnerable: FromSqlRaw concat
ctx.Users.FromSqlRaw("SELECT * FROM Users WHERE Email = '" + email + "'")Raw methods are only safe with {0} placeholders and separate argumentsORMs & Raw-Query Pitfalls
Prisma: safe
await prisma.$queryRaw`SELECT * FROM "User" WHERE email = ${email}`Tagged template: values become parametersPrisma: vulnerable
await prisma.$queryRawUnsafe(
`SELECT * FROM "User" WHERE email = '${email}'`
)The Unsafe variants accept a plain string; pass values as extra arguments if you must use themSQLAlchemy: safe
from sqlalchemy import text
conn.execute(
text("SELECT * FROM users WHERE email = :email"),
{"email": email},
)SQLAlchemy: vulnerable
conn.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))text() does not sanitise an f-stringDjango
User.objects.raw("SELECT * FROM auth_user WHERE email = %s", [email])Avoid .extra() and string-built RawSQL; the ORM filter API is parameterizedSequelize / Knex / TypeORM
sequelize.query('... WHERE email = :email', { replacements: { email } })
knex('users').whereRaw('email = ?', [email])
qb.where('u.email = :email', { email })Rule of thumbORM query builders are safe by default; every raw, unsafe, literal or whereRaw escape hatch needs placeholders
Identifiers, ORDER BY & LIMIT
WhyPlaceholders bind values only. Table names, column names, sort direction and SQL keywords cannot be parameterized.
Allow-list columns
const SORTABLE = { name: 'name', created: 'created_at' };
const col = SORTABLE[req.query.sort] ?? 'created_at';Map user input to known identifiers; never pass it throughAllow-list direction
const dir = req.query.dir === 'asc' ? 'ASC' : 'DESC';
const sql = `SELECT * FROM posts ORDER BY ${col} ${dir} LIMIT $1`;Numeric LIMIT / OFFSET
const limit = Math.min(Number.parseInt(req.query.limit, 10) || 20, 100);Or bind them as parameters where the driver allows itDynamic table namesPrefer a fixed set of tables chosen by code; quote with the driver's identifier helper (psycopg.sql.Identifier, QUOTENAME, format('%I')) after allow-listing
LIKE searches
WHERE name LIKE $1 -- value: '%' + term + '%'Build the pattern in code and bind it; escape % and _ in the term if they should be literalStored Procedures & Dynamic SQL
Not automatically safeA stored procedure is only safe if it does not build SQL strings from its parameters internally
Vulnerable T-SQL
SET @sql = 'SELECT * FROM Users WHERE Email = ''' + @Email + '''';
EXEC(@sql);Safe T-SQL
EXEC sp_executesql
N'SELECT * FROM Users WHERE Email = @Email',
N'@Email NVARCHAR(256)',
@Email = @Email;T-SQL identifiers
SET @sql = N'SELECT * FROM ' + QUOTENAME(@TableName);Combine with an allow-listSafe PL/pgSQL
EXECUTE format('SELECT count(*) FROM %I WHERE owner = $1', tbl)
USING owner_id;%I quotes identifiers; USING binds valuesCalling procedures
CALL / EXEC with bound parameters from the clientConcatenating the procedure call itself reintroduces the problemDefense in Depth
Least-privilege accounts
GRANT SELECT, INSERT, UPDATE ON orders TO app_rw;
REVOKE ALL ON SCHEMA public FROM PUBLIC;The app should not connect as a superuser, root, sa or the table ownerSeparate rolesDifferent accounts for the web app, migrations, reporting and admin tasks; read-only where possible
Input validationValidate type, length and format (integer IDs, UUIDs, enums) at the boundary; it reduces risk but does not replace parameters
Generic error messagesLog database errors server-side; never return raw SQL errors or stack traces to clients
Escaping as a last resortDatabase-specific escaping functions are fragile (encodings, quoting modes). Use only for legacy code that cannot be parameterized yet
WAF is supplementaryA web application firewall can catch noisy attempts but is easily bypassed; fix the code
Limit blast radiusRow Level Security, views exposing only needed columns, and encrypted sensitive columns reduce damage if a flaw slips through
Testing & Detection
Code review search
grep -rnE '(query|execute|raw)\(.*(\+|\$\{|f")' src/Flags SQL calls built with +, ${} or f-strings for manual reviewSASTStatic analysis such as Semgrep, CodeQL or SonarQube detects tainted input flowing into SQL sinks; run it in CI
DASTDynamic scanners such as OWASP ZAP test a running staging app for injection points
Specialist toolssqlmap and similar tools automate SQLi testing; use them only against systems you own or have written permission to test
Unit testsAssert that inputs containing quotes, semicolons and comment markers are stored and returned literally
MonitoringAlert on spikes of SQL syntax errors and unusual query patterns in database and application logs
OWASP referencesOWASP SQL Injection Prevention Cheat Sheet and Query Parameterization Cheat Sheet: cheatsheetseries.owasp.org
Keep the references coming
One email a week: new and trending developer tools, fresh comparisons, and what shipped. Unsubscribe in one click.

