arrow_backBack to field notes
WEB SECURITY Published 8 Aug 2026

What Is SQL Injection and How Do You Prevent It?

A practical breakdown of SQL injection attacks, real examples, and the concrete fixes that actually stop them in production code.

SQL injection has been around since the late 1990s and it's still one of the most common ways web applications get compromised. The bug is simple: an attacker sends input that gets interpreted as SQL code instead of plain data, and the database does something it was never supposed to do.

How the attack actually works

Imagine a login form that builds a query like this in PHP:

$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";

If the app doesn't sanitize input, an attacker types admin' -- into the username field. The query becomes:

SELECT * FROM users WHERE username = 'admin' --' AND password = ''

The -- comments out the rest of the line, so the password check never happens. That's a classic auth bypass. Attackers also use UNION SELECT to pull data from other tables, or stack queries with a semicolon to run something destructive like DROP TABLE users; if the driver allows multiple statements.

There's also blind SQL injection, where the app doesn't show query results directly. Attackers infer information through timing (SLEEP(5) in MySQL) or boolean responses (does the page behave differently for a true vs. false condition). Tools like sqlmap automate this kind of extraction once a vulnerable parameter is found.

Why string concatenation is the root problem

Every variant of this attack comes back to one thing: mixing code and data in the same string. The database can't tell the difference between a legitimate value and injected syntax because they arrive in the same channel. Escaping quotes helps in some cases but it's fragile — different encodings, second-order injection (data stored once, then reused unsafely later), and driver-specific quirks all create bypass paths. Escaping is a patch, not a fix.

Parameterized queries are the real fix

The fix is to separate SQL code from user data at the driver level, using parameterized queries (also called prepared statements). The database receives the query structure first, then binds values afterward, so user input can never change the query's meaning.

In Python with psycopg2:

cur.execute("SELECT * FROM users WHERE username = %s AND password = %s", (username, password))

In Node.js with mysql2:

connection.execute('SELECT * FROM users WHERE username = ? AND password = ?', [username, password]);

In Java with JDBC:

PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE username = ? AND password = ?");
stmt.setString(1, username);
stmt.setString(2, password);

Notice the pattern: placeholders (%s, ?) hold the spot for data, and the actual values get passed separately. No string concatenation, no manual escaping needed. This works for the huge majority of queries you'll write in a normal application.

What about dynamic table or column names?

Parameterization handles values but not identifiers — you can't bind a table name as a parameter. If your app needs to select a table dynamically (rare, and often a design smell), whitelist the allowed values against a hardcoded list instead of trusting user input directly:

allowed_tables = {'orders', 'invoices', 'customers'}
if table_name not in allowed_tables:
    raise ValueError("Invalid table")

Never build identifier names through string formatting from user input, even with escaping.

Layered defenses beyond the query itself

Parameterized queries are the primary control, but a few other things matter:

  • Least privilege on the database account. The app's DB user shouldn't have DROP, ALTER, or access to unrelated schemas. If an injection does slip through, limited privileges cap the damage.
  • ORMs help by default. Django's ORM, SQLAlchemy, and Hibernate all parameterize queries automatically when you use their standard query-building methods. The risk reappears when developers drop into raw SQL or use .extra()/text() calls with string interpolation — so audit those spots specifically.
  • Input validation is a secondary layer, not a replacement. Checking that an email field looks like an email is good practice, but it doesn't stop injection on its own — attackers find creative payloads that still pass loose validation.
  • A WAF can catch known attack patterns, but it's a detection layer, not a fix for the underlying code.

Testing your own code for this bug

Run your queries through a static analysis tool (Bandit for Python, Semgrep with SQL injection rulesets) as part of CI. For manual testing, try injecting a single quote (') into every input field and watch for SQL error messages leaking in the response — that's often the first sign a query isn't parameterized.

If you want to go deeper on this, Korra Studio's Web Security track covers injection alongside XSS and auth bypass, and the Databases segments walk through query design patterns that avoid this class of bug entirely.

Written with AI assistance, reviewed and published by Michal Pilch (CISSP), Korra Studio.

Ready to go further?

This is one note from the Korra Studio knowledge base — the platform pairs every topic with 1-to-1 mentoring.

Get started freearrow_forward