Safeguard
Security

SQLi Cheat Sheet: Detection and Defense Guide

A practical SQLi cheat sheet covering how injection works, the patterns to recognize, and the parameterized-query defenses that actually stop it.

Marcus Chen
DevSecOps Engineer
6 min read

This SQLi cheat sheet is a defensive reference: it explains how SQL injection happens, the signatures that betray a vulnerable query, and the coding patterns that close the hole for good. SQL injection stays near the top of the OWASP risk lists year after year, not because it is hard to fix, but because one string-concatenated query anywhere in a codebase reopens it.

Nothing here is a working exploit against a live system. The goal is to help you find and fix the vulnerable patterns in your own code before someone else finds them.

Why injection happens

SQL injection occurs when untrusted input is glued directly into a query string, so the database can no longer tell your intended SQL apart from the attacker's added SQL. The canonical bad pattern looks like this:

# Vulnerable: user input concatenated into the query
query = "SELECT * FROM users WHERE email = '" + user_input + "'"
cursor.execute(query)

If user_input contains a quote, everything after it is interpreted as SQL rather than data. The fix is not to filter the quote. The fix is to stop building queries out of strings.

The patterns a reviewer should flag

When reading code or a scan report, these are the shapes that mean "possible injection here":

  • String concatenation or interpolation that builds a query from a variable.
  • Format strings such as f"... {value} ..." or String.format feeding into execute.
  • ORM "raw" escape hatches like raw(), .exec(), or a query builder's literal-SQL method receiving user data.
  • Dynamic table or column names taken from request parameters, which parameterization alone cannot fix.

Grep is your friend for a first pass. Searching for execute( next to + or an f-string across a repo surfaces most candidates in minutes.

Injection categories, conceptually

Understanding the classes helps you reason about detection without needing payloads:

In-band injection returns data in the same response, either by adding a UNION that appends attacker-chosen columns or by triggering a database error that leaks schema. Verbose SQL errors reaching the client are a strong smell.

Blind injection reveals nothing directly. The attacker asks true/false questions and reads the answer from a difference in the response, either its content (boolean-based) or its timing (time-based, using a sleep function). If your app behaves observably differently for a malformed input, blind injection may be feasible.

Out-of-band injection makes the database itself open a network connection the attacker controls, useful when the response channel is closed.

You do not need the payloads to defend against any of these. The single control below neutralizes all of them for the data path.

The one defense that works: parameterized queries

Prepared statements send the SQL structure and the data to the database separately. The data is never parsed as SQL, so a quote is just a quote.

# Safe: the driver binds the value, never parses it as SQL
query = "SELECT * FROM users WHERE email = %s"
cursor.execute(query, (user_input,))
// Safe: PreparedStatement with a bound parameter
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM users WHERE email = ?");
ps.setString(1, userInput);
ResultSet rs = ps.executeQuery();

Every mainstream driver supports this, and there is no performance reason to avoid it. If a library forces you to build SQL strings, treat that as a design bug.

Handling the cases parameters cannot bind

You cannot bind a table name, column name, or ORDER BY direction as a parameter, because those are query structure, not data. For those, use an allowlist:

# Column name cannot be a bound parameter; validate against a fixed set
ALLOWED_SORT = {"created_at", "email", "name"}
if sort_column not in ALLOWED_SORT:
    raise ValueError("invalid sort column")
query = f"SELECT * FROM users ORDER BY {sort_column}"

The value that reaches the string is chosen from a set you control, never from raw input. This is the only safe way to make identifiers dynamic.

Defense in depth

Parameterization is the primary control. Layer these on top:

  • Least-privilege database accounts. The app's login user should not own the schema or hold DROP rights. A successful injection then has a small blast radius.
  • Input validation as a secondary filter, not the primary defense. Reject an email that is not shaped like an email before it ever reaches the query.
  • Stored-procedure discipline, remembering that a procedure that itself concatenates input is just as vulnerable.
  • Continuous testing. A dynamic scanner probes running endpoints for injectable parameters. Our DAST scanner flags reachable injection points, and for the ORM and driver versions your project pins, an SCA tool such as Safeguard can flag a dependency with a known injection CVE transitively.

Finding it in an existing codebase

A retrofit follows a repeatable loop. Enumerate every place code talks to the database, classify each as parameterized or string-built, rewrite the string-built ones, and add a regression test that feeds a quote through the endpoint and asserts a clean result rather than an error. Static analysis in CI catches new concatenated queries before they merge, which stops the codebase from backsliding. The Safeguard Academy has a longer secure-coding track if you want structured practice.

FAQ

Do prepared statements slow down my queries?

No. Prepared statements are often faster because the database can reuse a cached execution plan across calls. The separation of code and data is a correctness and security property, not a performance tax.

Is escaping input enough to stop SQL injection?

Manual escaping is fragile and encoding-dependent, and it fails the moment someone forgets one call site. Parameterized queries move the responsibility to the driver and are the recommended defense. Treat escaping as a last resort for legacy code you cannot yet refactor.

Does an ORM make me safe from SQL injection?

Mostly, as long as you use its parameterized query methods. Every major ORM exposes a raw-SQL escape hatch, and passing user input into that reintroduces the flaw. Audit the raw-query calls specifically.

Can a scanner detect blind SQL injection?

Yes. Dynamic scanners detect blind injection by measuring response differences and timing behavior across probes, though blind flaws take longer to confirm than error-based ones. Pair automated scanning with code review for the highest confidence.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.