SQL Injection (SQLi) remains one of the most dangerous and widespread web application vulnerabilities. Listed as the #3 risk in the OWASP Top 10 (2021) under Injection, it allows attackers to interfere with the queries an application sends to its database. Despite being well-understood for over two decades, SQLi continues to appear in production systems — often with devastating consequences.

What Is SQL Injection?

SQL Injection occurs when untrusted user input is concatenated directly into a SQL query without proper sanitization or parameterization. This allows an attacker to alter the query’s logic, extract sensitive data, bypass authentication, or even execute commands on the underlying operating system.

Consider a simple login form. Behind the scenes, the application might construct a query like this:

SELECT * FROM users WHERE username = 'admin' AND password = 'secret123';

If the application naively inserts user input into this query, an attacker can supply a crafted payload — such as ' OR '1'='1 — to manipulate the logic entirely:

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

This evaluates to TRUE for every row, effectively bypassing authentication.

Types of SQL Injection

Not all SQLi attacks look the same. Understanding the different types is crucial for both exploitation during penetration tests and building effective defenses.

Classic (In-Band) SQLi

The most straightforward type. The attacker sends a malicious payload and receives the result directly in the application’s response. Error-based SQLi leverages database error messages to extract information, while union-based SQLi uses the UNION operator to combine results from additional queries.

-- Union-based extraction of usernames and passwords
' UNION SELECT username, password FROM users --

Blind SQLi (Boolean-Based)

When the application does not display query results or error messages, attackers can still infer data by observing different application behaviors (e.g., a page loads normally vs. returns a generic error).

-- If the page loads normally, the first character of the admin password is 'a'
' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin')='a' --

Time-Based Blind SQLi

When there’s no visible difference in the response, attackers use time delays to extract data one bit at a time. If the response is delayed, the condition is true.

-- If the response takes 5 seconds, the database is MySQL
' OR IF(1=1, SLEEP(5), 0) --

Union-Based SQLi

This technique appends a UNION SELECT statement to the original query, allowing the attacker to retrieve data from other tables. The attacker must first determine the number of columns in the original query:

' ORDER BY 3 --          -- Find column count
' UNION SELECT NULL, NULL, NULL --  -- Match column count
' UNION SELECT table_name, NULL, NULL FROM information_schema.tables --

Vulnerable Code Examples

PHP — The Classic Mistake

<?php
// ❌ VULNERABLE — direct string concatenation
$username = $_POST['username'];
$password = $_POST['password'];

$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);

Python (Flask) — Dangerous f-strings

# ❌ VULNERABLE — unsanitized input in raw query
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
    db.execute(query)

Secure Code: Parameterized Queries

The most effective defense against SQLi is parameterized queries (also known as prepared statements). The database engine treats user input strictly as data, never as executable SQL.

PHP — Prepared Statements with PDO

<?php
// ✅ SAFE — parameterized query with PDO
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([
    ':username' => $_POST['username'],
    ':password' => $_POST['password']
]);
$user = $stmt->fetch();

Python — Parameterized Query

# ✅ SAFE — parameterized query
@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    query = "SELECT * FROM users WHERE username = %s AND password = %s"
    cursor.execute(query, (username, password))

Using an ORM as a Defense Layer

Object-Relational Mappers (ORMs) like SQLAlchemy (Python), Eloquent (PHP/Laravel), or Prisma (Node.js) abstract raw SQL away entirely. They build parameterized queries under the hood, significantly reducing the attack surface.

# ✅ SAFE — SQLAlchemy ORM query
user = session.query(User).filter_by(
    username=request.form['username'],
    password=request.form['password']
).first()

Warning: ORMs are not a silver bullet. Avoid using raw query methods like session.execute(text(...)) with unsanitized input — this reintroduces the same vulnerability.

Testing with sqlmap

During a penetration test, sqlmap is the go-to tool for automated SQLi detection and exploitation. It supports all major database engines and injection techniques.

# Basic scan against a URL parameter
sqlmap -u "https://target.com/page?id=1" --dbs

# Test a POST login form
sqlmap -u "https://target.com/login" --data="username=admin&password=test" --level=3 --risk=2

# Dump a specific table
sqlmap -u "https://target.com/page?id=1" -D mydb -T users --dump

# Use a cookie for authenticated testing
sqlmap -u "https://target.com/dashboard?id=5" --cookie="session=abc123" --batch

The --level flag increases the number of payloads tested, while --risk controls how aggressive the injections are. Always obtain written authorization before running sqlmap against any target.

WAF Rules and Additional Defenses

A Web Application Firewall (WAF) adds an extra layer of protection by inspecting HTTP traffic and blocking known SQLi patterns. Common rules include:

  • Blocking requests containing UNION SELECT, OR 1=1, SLEEP(, and BENCHMARK(
  • Rejecting inputs with single quotes, double dashes (--), or semicolons in unexpected fields
  • Rate-limiting repeated suspicious requests from the same IP

Popular WAFs like ModSecurity (with the OWASP Core Rule Set), Cloudflare WAF, and AWS WAF offer preconfigured SQLi rulesets. However, WAFs should never be your only defense — they can be bypassed with encoding tricks, case manipulation, and inline comments.

OWASP Recommendations

The OWASP Foundation provides clear guidance for preventing injection attacks:

  1. Use parameterized queries — This is the primary defense. Every database driver supports them.
  2. Use stored procedures — When properly parameterized, they provide an additional abstraction layer.
  3. Validate and sanitize input — Apply allowlists for expected data formats (e.g., numeric IDs should only contain digits).
  4. Escape all user-supplied input — As a last resort when parameterization isn’t possible.
  5. Apply least privilege — Database accounts used by the application should have minimal permissions. Never connect as root or sa.
  6. Keep software updated — Database engines, drivers, and frameworks regularly patch injection-related bugs.

Final Thoughts

SQL Injection is a solved problem in theory — parameterized queries eliminate it entirely. Yet it persists in practice because developers still concatenate strings, legacy code goes unpatched, and security testing happens too late in the development cycle.

Whether you’re a developer writing code or a pentester breaking it, understanding SQLi at a deep level is non-negotiable. Use prepared statements by default, test with sqlmap in authorized engagements, and treat every user input as hostile. Your database will thank you.