SQL injection (SQLi) has appeared in the OWASP Top 10 every year since the list was created. Despite being a well-understood vulnerability, it continues to cause major breaches — from Sony Pictures in 2011 to hundreds of smaller applications every month. Understanding how to detect and prevent it is a fundamental security skill.

What Is SQL Injection?

SQL injection occurs when an application includes untrusted data in a SQL query without proper sanitization. Instead of treating user input as data, the database interpreter treats it as SQL code. The result: attackers can read, modify, or delete arbitrary data, bypass authentication, and sometimes execute commands on the server.

A classic example. Consider this login query:

SELECT * FROM users WHERE username = '$username' AND password = '$password'

If an attacker enters ' OR '1'='1 as the username, the query becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...'

Since '1'='1' is always true, the attacker logs in without a valid password.

The Three Types of SQL Injection

1. Classic (In-Band) SQL Injection

The application returns database errors or query results directly in the HTTP response. This is the easiest to detect. Send a single apostrophe (') as a parameter value and look for database error messages in the response:

Any of these messages in a response body is a confirmed SQL injection vulnerability.

2. Blind SQL Injection

The application does not return error messages, but its behavior changes based on whether the injected condition is true or false. You can detect it by comparing responses to boolean conditions:

GET /product?id=1 AND 1=1    → Normal response (200 OK)
GET /product?id=1 AND 1=2    → Different response (empty page, 404, etc.)

If the responses differ, the application is evaluating your SQL conditions — confirming injection.

3. Time-Based Blind SQL Injection

When the application's responses look identical regardless of injected conditions, time-based techniques can still detect SQLi. The idea: inject a conditional sleep and measure the response time.

GET /product?id=1; IF(1=1) WAITFOR DELAY '0:0:5'--   (MSSQL)
GET /product?id=1 AND SLEEP(5)--                      (MySQL)
GET /product?id=1; SELECT pg_sleep(5)--               (PostgreSQL)

If the response takes approximately 5 seconds longer than normal, SQL injection is present.

Where to Look for SQL Injection

Every point where user input touches a database query is a potential injection vector:

How to Fix SQL Injection

Parameterized Queries (Primary Defense)

Never concatenate user input into SQL strings. Use parameterized queries (also called prepared statements). The database treats the parameter as data, never as code.

# Vulnerable
query = "SELECT * FROM users WHERE email = '" + email + "'"

# Safe — Python with sqlite3
cursor.execute("SELECT * FROM users WHERE email = ?", (email,))

# Safe — Python with psycopg2
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

ORMs (Secondary Defense)

Object-Relational Mappers like SQLAlchemy, Django ORM, and Hibernate generate parameterized queries by default. They are safe as long as you do not use raw query methods (text(), raw()) with user input.

Input Validation

Validate that inputs match expected formats. An order ID should be an integer — reject anything else before it reaches the query layer. This is a defense-in-depth measure, not a substitute for parameterized queries.

Least Privilege

The database user your application connects with should only have the permissions it actually needs. A product catalog reader does not need DELETE privileges. This limits the damage when injection does occur.

Testing Your Application

Manual testing covers the basics: try ', ", ;, --, and boolean conditions in every input field. For thorough coverage, use an automated scanner. Shieldome tests for SQL injection indicators across all detected parameters, looking for database error signatures without sending destructive payloads.

The key principle: detect the vulnerability, never exploit it. Confirming that a single-quote causes an error is enough to flag the issue without touching real data.