This SQL injection prevention cheat sheet gives developers actionable, code-level techniques to stop one of the oldest and still most exploited web application vulnerabilities. It covers attack types, safe coding patterns across five languages, and the testing practices needed to confirm your defenses actually hold up.

SQL injection (SQLi) has been a top-tier web application risk for more than two decades, and it hasn’t gone away. Injection is still ranked among the five most critical risks in the OWASP Top 10, and it was central to some of the highest-profile breaches of the past few years. These included the widespread MOVEit Transfer attacks of 2024 as well as a SQL injection flaw in PostgreSQL (CVE-2025-1094) that enabled a BeyondTrust Remote Support zero-day, with affected entities including the US Treasury Department.
The vulnerability class itself hasn’t changed. What changes is how much of your codebase, your ORM’s escape hatches, and your API surface you’ve actually checked.
SQL injection prevention is the set of secure coding practices that stop untrusted input from altering the structure of a SQL query. The core technique is the use of parameterized queries (prepared statements), which separate SQL code from user-supplied data at the database driver level so that input can never be interpreted as part of the query itself. Input validation, least-privilege database access, and continuous DAST scanning all support that core defense.
SQL injection vulnerabilities are a wide class of security flaws that let an attacker supply database commands through application input and have them executed by the database server. The main variants differ in where and how the payload surfaces:
For a wide set of example payloads by injection type, see the Invicti SQL injection cheat sheet.
Directly including user data in a query string is inherently unsafe. Concatenating user input with SQL code mixes application data and application logic in the same string, leaving room for an attacker to control what the database executes. An insecure concatenated query in PHP might look like this:
$query = "SELECT * FROM users WHERE userid = " . $_GET['userid'];If an attacker can control the value of the userid parameter, the application is vulnerable to SQL injection, regardless of what framework or language sits around this line of code.
The number one rule for preventing SQL injection is: never insert user input directly into SQL code. Whether you’re working with MySQL, PostgreSQL, Oracle, or Microsoft SQL Server, use parameterized queries or prepared statements everywhere user input reaches a database call. The database engine compiles the query structure before binding the input values, so special characters in the input can’t alter the query’s logic.
Using the JDBC PreparedStatement API, the ? placeholder holds the position of the value and setInt binds it after the query is already compiled:
// Make sure conn is a valid open Connection, and handle exceptions properly in production code
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE userid = ?");
stmt.setInt(1, userId); // userId holds the user-supplied value
ResultSet rs = stmt.executeQuery();PHP Data Objects (PDO) uses the same idea with named placeholders, which can make longer queries easier to read than positional ones:
// Remember to set PDO::ATTR_ERRMODE to handle errors properly
$stmt = $pdo->prepare("SELECT * FROM users WHERE userid = :userid");
$stmt->execute(['userid' => $userId]); // $userId holds the user-supplied valueThe %s placeholder here is a driver-level parameter marker, not Python string formatting, so it’s safe even though it looks similar:
# PostgreSQL with psycopg2 – parameterized query
cursor.execute("SELECT * FROM users WHERE userid = %s", (user_id,))The same pattern holds for asynchronous drivers: the placeholder and the bound value travel to the database separately, regardless of how the surrounding code is structured:
// MySQL with mysql2 – parameterized query
const [rows] = await conn.execute(
'SELECT * FROM users WHERE userid = ?',
[userId] // userId is bound as a parameter, not concatenated
);Parameterization is what stops injection, but input validation still matters for resilience: it catches malformed data early and shrinks the attack surface before a request ever reaches the query layer. Use allowlists for fields with a known, closed set of valid values, and check that integers are really integers and strings match expected patterns.
Here’s a validation example for PHP:
if (!filter_var($userid, FILTER_VALIDATE_INT)) {
throw new Exception("Invalid userid");
}This rules out malicious string values in the userid parameter, but note that it validates format only – it still doesn’t replace the need for a parameterized query.
Stored procedures can improve security but only when they use parameterized inputs and avoid dynamic SQL internally. A procedure that builds a query by concatenating its own input parameters and then runs it with EXEC or sp_executesql reintroduces the same injection risk you were trying to remove.
The safe pattern is a fixed query structure with parameterized inputs, as in this Oracle example:
CREATE PROCEDURE GetUser(p_userid IN NUMBER) AS
BEGIN
SELECT * FROM users WHERE userid = p_userid;
END;Your application code then calls this procedure with a bound parameter, the same way it would call a prepared statement, rather than building the call by concatenating input into a dynamic SQL string.
Modern object-relational mapping (ORM) frameworks such as Hibernate, Django ORM, and SQLAlchemy parameterize queries automatically when you use their standard query APIs. This is ostensibly more secure by default, but risk can still show up if developers reach for the ORM’s raw-query escape hatch to handle a complex query, and build that raw string with concatenation instead of parameters.
Safe and unsafe examples for Hibernate:
// Safe: HQL with a named parameter
Query query = session.createQuery("FROM User WHERE id = :userId");
query.setParameter("userId", userId);
// Unsafe: string concatenation in HQL
Query unsafeQuery = session.createQuery("FROM User WHERE id = " + userId);The same pattern applies in Django ORM:
# Django ORM – safe by default
User.objects.filter(id=user_id)
# Unsafe: raw() built with an f-string
User.objects.raw(f"SELECT * FROM auth_user WHERE id = {user_id}")
# Safe: raw() but with a bound parameter
User.objects.raw("SELECT * FROM auth_user WHERE id = %s", [user_id])Treat any raw SQL inside an ORM-based codebase with the same scrutiny you’d apply to an application with no ORM at all.
REST and GraphQL endpoints are exposed to the same injection risk as traditional web forms, and the attack surface is often larger and less visible: path parameters, query strings, JSON request bodies, and GraphQL field arguments can all end up inside a database query. A GraphQL resolver that interpolates an argument into a query string is just as vulnerable as an unparameterized SQL statement anywhere else:
// Unsafe GraphQL resolver
user: (_, { id }) => db.query('SELECT * FROM users WHERE id = ' + id);
// Safe GraphQL resolver (parameterized)
user: (_, { id }) => db.query('SELECT * FROM users WHERE id = $1', [id]);Automated API security testing should cover these input paths specifically, since they’re easy to miss in a manual review focused on form fields.
Avoid exposing detailed error messages that reveal table names, column names, or query structure. Return generic error messages to users and log the details server-side. Standardizing your error handling this way can also make some timing-based attacks harder to pull off.
Monitoring helps you catch unusual query patterns, such as repeated Boolean conditions or time-based delays, that point to blind SQL injection attempts. Watch application and database logs for unexpected SQL commands and failed authentication attempts. As a general precaution, sensitive data should never be written to logs in the first place.
Update your application components, ORMs, database drivers, the DBMS itself, and the underlying operating system on a regular schedule. This applies equally to open-source and commercial platforms, from MySQL and PostgreSQL to Oracle and Microsoft SQL Server. Staying up-to-date limits your exposure to newly disclosed vulnerabilities, including in the database engine itself: the PostgreSQL flaw behind the BeyondTrust breach is a reminder that the database layer, not just application code, needs to stay patched.
This won’t stop an attack from happening, but your application should connect to the database using a low-privilege account limited to the tables and columns it actually needs. If an attacker does find and exploit a SQL injection flaw somewhere, least privilege limits what they can do with it. The database server itself should also run with only the permissions and features it needs.
A web application firewall can catch many SQL injection attempts through signature matching, but attackers can and do bypass WAF rules using encoding tricks, comment sequences, and unusual syntax that signature-based detection doesn’t anticipate. Treat parameterized queries as the primary control and a WAF as a secondary layer of defense in depth, not the other way around.
Every one of the practices above reduces risk, but the only way to know your application is actually safe is to test it, and test it again at every stage of development. Static application security testing (SAST) can flag risky patterns, such as string concatenation in a query or an unparameterized ORM raw call, while code is still being written. Dynamic application security testing (DAST) is what confirms whether the running application is actually exploitable, including SQL injection classes that never surface in source code review.
The Invicti Platform tests for all major SQL injection types, including in-band, Boolean-based blind, time-based blind, and out-of-band injection. For blind and out-of-band cases, where no evidence of the injection appears in the HTTP response, Invicti OOB confirms exploitation through an external callback, so a finding comes with proof rather than a guess. Combined with proof-based scanning, that means confirmed SQL injection findings arrive with a proof of exploit attached, so your team can prioritize what’s real instead of triaging every possible alert by hand – and with DAST-SAST correlation, many SQLi findings are automatically deduplicated.
Preventing SQL injection takes more than a single fix. It’s the combination of parameterized queries, careful ORM and API handling, input validation, least privilege, and continuous testing that keeps this decades-old vulnerability class out of your production environment. Pair the practices in this cheat sheet with a DAST-first approach to application security, and you’ll catch what manual review and static analysis alone tend to miss. Request a demo of the Invicti Platform to see this at work in your app and API environments.
Parameterized queries, also called prepared statements, are the most effective defense. They separate SQL code from user-supplied data at the database driver level, so the database has already defined what is code and what is data before your input ever reaches it. No amount of escaping or string sanitization achieves the same guarantee.
No. Validation reduces the attack surface by rejecting input that doesn’t match an expected format, but it isn’t a substitute for parameterized queries. Legitimate inputs like names, addresses, or search terms can legally contain SQL metacharacters, validation logic can be misconfigured or bypassed, and second-order SQL injection can slip through validation entirely because the payload only becomes dangerous later, in a different query. Use validation alongside parameterized queries, not instead of them.
Only if they use parameterized inputs and avoid building SQL dynamically inside the procedure. A stored procedure that concatenates its inputs into a string and executes it with EXEC or sp_executesql carries the same risk as unparameterized inline SQL.
Mostly, but not completely. Standard ORM query APIs parameterize automatically, but every major ORM also offers a raw-query escape hatch for complex queries, and those raw calls are only as safe as the code that builds them. Review any raw SQL in an ORM-based codebase with the same scrutiny you’d apply without an ORM.
A WAF can block many known injection patterns, but it can be bypassed with encoding tricks and unusual syntax that signature matching doesn’t catch. Use a WAF as a secondary layer of defense, with parameterized queries as the primary control.
It’s an injection where the malicious payload is stored safely on input and only triggers later, when the application reuses that stored value inside a different query. It’s easy to miss in testing because nothing goes wrong at the point where the data was originally entered. The prevention is the same as for any other SQL injection: parameterized queries everywhere stored data is used in a query, not just at the original entry point.
The same way you prevent it anywhere else: parameterized queries on every input path, including REST path and query parameters, JSON request bodies, and GraphQL field arguments. API endpoints often have a larger and less obvious attack surface than web forms, so automated API security testing should cover these paths specifically rather than relying on manual review of form fields alone.
Use SAST during development to flag risky code patterns, and DAST against a running application or staging environment to confirm whether those patterns are actually exploitable. The Invicti platform detects all major SQL injection classes, including blind and out-of-band injection confirmed through Invicti OOB, and reports confirmed findings with a proof of exploit through proof-based scanning.
