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 flaw
Safe pattern (any language)"SELECT * FROM users WHERE email = ?" + [email]Placeholder in the SQL, value passed separately; the driver never parses the value as SQL

Node.js (pg, mysql2)

Vulnerable: pgawait client.query( `SELECT * FROM users WHERE email = '${email}'` );A template literal is still string concatenation
Safe: pgawait client.query( 'SELECT * FROM users WHERE email = $1', [email] );PostgreSQL uses numbered placeholders $1, $2, ...
Safe: pg IN listawait client.query( 'SELECT * FROM users WHERE id = ANY($1)', [ids] // JS array -> Postgres array );
Vulnerable: mysql2conn.query("SELECT * FROM users WHERE id = " + req.params.id);
Safe: mysql2 executeconst [rows] = await conn.execute( 'SELECT * FROM users WHERE email = ? AND active = ?', [email, true] );execute() uses a server-side prepared statement
mysql2 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-stringcur.execute(f"SELECT * FROM users WHERE email = '{email}'")
Safe: psycopg 3cur.execute( "SELECT * FROM users WHERE email = %s", (email,), )%s is a placeholder here, not Python string formatting; note the 1-tuple
Safe: named paramscur.execute( "SELECT * FROM users WHERE email = %(email)s", {"email": email}, )
Never do thiscur.execute("... WHERE email = '%s'" % email)The % operator formats before the driver sees the query
Safe: sqlite3cur.execute("SELECT * FROM users WHERE email = ?", (email,)) cur.execute("... WHERE email = :email", {"email": email})
Identifiers: psycopg.sqlfrom 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 name

Java (JDBC, JPA, Spring)

Vulnerable: StatementStatement st = conn.createStatement(); st.executeQuery("SELECT * FROM users WHERE email = '" + email + "'");
Safe: PreparedStatementPreparedStatement ps = conn.prepareStatement( "SELECT * FROM users WHERE email = ?"); ps.setString(1, email); ResultSet rs = ps.executeQuery();Parameter indexes start at 1
Safe: JPA / JPQLem.createQuery( "SELECT u FROM User u WHERE u.email = :email", User.class) .setParameter("email", email) .getResultList();
Vulnerable: JPQL concatem.createQuery("SELECT u FROM User u WHERE u.email = '" + email + "'")JPQL and HQL injection are real; ORMs do not save you from concatenation
Safe: Spring JdbcTemplatejdbcTemplate.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 automatically

PHP (PDO, mysqli)

Vulnerable$pdo->query("SELECT * FROM users WHERE email = '$email'");Variable interpolation in double quotes is concatenation
Safe: 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 errors
Safe: mysqli$stmt = $mysqli->prepare('SELECT * FROM users WHERE email = ?'); $stmt->bind_param('s', $email); $stmt->execute();'s' = string, 'i' = integer

C# / .NET (ADO.NET, Dapper, EF Core)

Vulnerablevar cmd = new SqlCommand( "SELECT * FROM Users WHERE Email = '" + email + "'", conn);
Safe: SqlCommand parametersvar 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 AddWithValue
Safe: Dappervar user = conn.QuerySingleOrDefault<User>( "SELECT * FROM Users WHERE Email = @Email", new { Email = email });
Safe: EF Core FromSqlctx.Users.FromSql($"SELECT * FROM Users WHERE Email = {email}")Interpolated values become DbParameters (EF Core 7+; FromSqlInterpolated earlier)
Vulnerable: FromSqlRaw concatctx.Users.FromSqlRaw("SELECT * FROM Users WHERE Email = '" + email + "'")Raw methods are only safe with {0} placeholders and separate arguments

ORMs & Raw-Query Pitfalls

Prisma: safeawait prisma.$queryRaw`SELECT * FROM "User" WHERE email = ${email}`Tagged template: values become parameters
Prisma: vulnerableawait prisma.$queryRawUnsafe( `SELECT * FROM "User" WHERE email = '${email}'` )The Unsafe variants accept a plain string; pass values as extra arguments if you must use them
SQLAlchemy: safefrom sqlalchemy import text conn.execute( text("SELECT * FROM users WHERE email = :email"), {"email": email}, )
SQLAlchemy: vulnerableconn.execute(text(f"SELECT * FROM users WHERE email = '{email}'"))text() does not sanitise an f-string
DjangoUser.objects.raw("SELECT * FROM auth_user WHERE email = %s", [email])Avoid .extra() and string-built RawSQL; the ORM filter API is parameterized
Sequelize / Knex / TypeORMsequelize.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 columnsconst SORTABLE = { name: 'name', created: 'created_at' }; const col = SORTABLE[req.query.sort] ?? 'created_at';Map user input to known identifiers; never pass it through
Allow-list directionconst dir = req.query.dir === 'asc' ? 'ASC' : 'DESC'; const sql = `SELECT * FROM posts ORDER BY ${col} ${dir} LIMIT $1`;
Numeric LIMIT / OFFSETconst limit = Math.min(Number.parseInt(req.query.limit, 10) || 20, 100);Or bind them as parameters where the driver allows it
Dynamic 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 searchesWHERE name LIKE $1 -- value: '%' + term + '%'Build the pattern in code and bind it; escape % and _ in the term if they should be literal

Stored Procedures & Dynamic SQL

Not automatically safeA stored procedure is only safe if it does not build SQL strings from its parameters internally
Vulnerable T-SQLSET @sql = 'SELECT * FROM Users WHERE Email = ''' + @Email + ''''; EXEC(@sql);
Safe T-SQLEXEC sp_executesql N'SELECT * FROM Users WHERE Email = @Email', N'@Email NVARCHAR(256)', @Email = @Email;
T-SQL identifiersSET @sql = N'SELECT * FROM ' + QUOTENAME(@TableName);Combine with an allow-list
Safe PL/pgSQLEXECUTE format('SELECT count(*) FROM %I WHERE owner = $1', tbl) USING owner_id;%I quotes identifiers; USING binds values
Calling proceduresCALL / EXEC with bound parameters from the clientConcatenating the procedure call itself reintroduces the problem

Defense in Depth

Least-privilege accountsGRANT 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 owner
Separate 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 searchgrep -rnE '(query|execute|raw)\(.*(\+|\$\{|f")' src/Flags SQL calls built with +, ${} or f-strings for manual review
SASTStatic 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.