TechEarl

SQL Injection in HTTP Requests: Every Vector Attackers Use

A practical map of every place SQL injection can live inside an HTTP request: query string, URL path, request body (form, JSON, XML, multipart filename), and every header from User-Agent to Authorization. Where attackers look, why developers miss each one, and where to start hardening.

Ishan Karunaratne⏱️ 11 min readUpdated
Share thisCopied
The full map of HTTP request fields that carry SQL injection: query string, body fields, headers, cookies, and method

The SQL injection deep dive covers the variants: how injection works (union, error, boolean blind, time blind, out-of-band, second-order, NoSQL). This article covers the other axis: where injection lives inside an HTTP request. Variants describe the technique (covered in the PortSwigger Web Security Academy SQL injection labs). Vectors describe the target.

Most articles and tutorials only show the easy case: a query-string parameter like ?id=1 flows into a WHERE id = $1 query, and the attacker submits 1 OR 1=1. That is one vector out of fifteen. Every other field the application reads from the request is a candidate, and the ones developers forget about (User-Agent, Referer, X-Forwarded-For) tend to be where real-world incidents land, because the validation logic that protects form fields never gets applied to headers.

In short: SQL injection is not just a query-string bug. Anywhere an HTTP request carries attacker-controlled bytes, and the server concatenates those bytes into a SQL query, the request field is a candidate. That includes URL path segments, every body format (form, JSON, XML, multipart filename), and most headers an application reads back into a query: User-Agent and Referer for analytics logging, X-Forwarded-For for geolocation and ban lookups, Host for multi-tenant routing, non-session Cookies for preference and A/B-bucket lookups, Authorization for hand-rolled API key schemes. The default sqlmap scan misses most headers because it only tests them at --level=2 (cookies), --level=3 (User-Agent, Referer), or --level=5 (Host and other common headers). Real-world incidents land in these vectors as often as in the query string, because analytics middleware is the most under-reviewed code in nearly every codebase.

The map

Every cell below is a place untrusted bytes can enter the application. If any of these flow into a SQL query without parameterisation, that cell is exploitable.

LayerVectorHow often it gets concatenated unsafely
URLQuery string parameters (?id=1)Very common, but most reviewed
URLPath segments (/products/1/details)Common; often overlooked because "it's in the URL"
Bodyapplication/x-www-form-urlencoded fieldsSame review pressure as query string
Bodyapplication/json fields and nested valuesIncreasingly common, often via ORM raw-query calls
Bodyapplication/xml elements and attributesRare now, pairs with XXE
Bodymultipart/form-data filenamesFrequently logged unsanitised
HeaderUser-AgentVery common in analytics tables
HeaderRefererCommon in click-attribution tables
HeaderX-Forwarded-For and friendsCommon in geolocation and audit tables
HeaderHost and X-Forwarded-HostSpecific to multi-tenant routing
HeaderCookie (non-session cookies)Common in preference/A-B lookups
HeaderAuthorization (custom tokens)Specific to hand-rolled API key schemes
HeaderAccept-LanguageSometimes written to user preferences
HeaderCustom (X-Api-Key, X-Tenant-Id, etc.)Depends on the app, often missed
OtherHTTP method (rare)Almost only in logging code

Below: one paragraph per vector, with a link to the dedicated deep-dive where it exists.

URL: query string parameters

The canonical case. GET /products?id=1 flows into SELECT * FROM products WHERE id = $1. The attacker sends ?id=1 UNION SELECT username, password, 1 FROM users-- - and gets credentials back in the response. Every SQLi tutorial starts here. Defence is parameterised queries plus type-validation on the route handler. See the SQL injection deep dive for the full mechanism and the variant walkthroughs.

URL: path segments

GET /products/1/reviews where 1 becomes the product ID. The router extracts the segment and the code does SELECT * FROM reviews WHERE product_id = '${id}'. Often overlooked because the parameter is "part of the URL itself" rather than a ?key=value pair, and because frameworks present path parameters as already-routed values that feel safer than they are. They are not safer. Test every numeric path segment for type-coercion bypass (e.g., /products/1' OR '1'='1/reviews) and every string segment for direct injection. sqlmap supports path-segment testing with -u and a * injection-point marker: sqlmap -u 'https://target.example/products/1*/reviews'.

Body: form-urlencoded fields

POST /login with username=admin&password=test. Same shape as the query string, just the body instead of the URL. Same defences. The classic worked example is in the SQL injection deep dive under the login bypass section.

Body: JSON fields

POST /api/v1/users/search with {"q":"foo","limit":10}. Modern attack surface. The mistake usually happens in ORM raw-query escape hatches (prisma.$queryRawUnsafe, Django raw(), Sequelize literal(), ActiveRecord find_by_sql) where the JSON field is interpolated into a template string. Also relevant for GraphQL variables passed to a custom resolver that builds SQL. Covered in detail in SQL injection in JSON request bodies.

Body: XML elements and attributes

POST /api/legacy with an XML payload. Rare in green-field apps but still alive in SOAP services and enterprise integration code. Vulnerable when the SAX/DOM parser hands the element text to a query builder unsanitised. Pairs naturally with XXE attacks when the same parser also has external entities enabled. The defence is the same (parameterise), plus a hardened XML parser configuration.

Body: multipart filename

POST /upload with Content-Disposition: form-data; name="file"; filename="report.pdf". The filename is attacker-controlled. Apps that log uploads ("user X uploaded file Y") often insert the filename directly: INSERT INTO uploads (user_id, filename) VALUES (?, '${filename}'). The injection lives in the filename. Defence is parameterised insert plus filename sanitisation before storage (kebab-case, allowlisted characters).

Header: User-Agent

The highest-volume header vector by far. Every analytics table, every audit log, every "track which browsers our users have" feature reads User-Agent and writes it somewhere. The pattern INSERT INTO page_views (path, user_agent, ip, timestamp) VALUES (?, '${ua}', ?, ?) is everywhere, and the unparameterised middle field is the bug. sqlmap tests User-Agent only at --level=3 or higher; the default level misses it. Full walkthrough with vulnerable code and exploit in User-Agent SQL injection.

Header: Referer

The Referer header reports the page that linked to the current request. Apps that do click-attribution analytics ("which sites are sending us traffic") store it. Same vulnerability pattern as User-Agent, slightly lower search volume but the same code shape. Many marketing-analytics plugins were the source of CVEs in the 2017-2019 window for exactly this. Deep dive: Referer header SQL injection.

Header: X-Forwarded-For (and X-Real-IP, X-Client-IP)

When the application sits behind a proxy or load balancer, the real client IP lives in X-Forwarded-For rather than the TCP socket. Many apps read this header to log the "real" IP, lookup geolocation, or check against a ban list. The lookup-against-a-ban-list pattern is the dangerous one: SELECT 1 FROM geo_blocks WHERE ip = '${xff}'. Attacker controls the header verbatim. Same family includes X-Real-IP, X-Client-IP, True-Client-IP, X-Forwarded (without the -For), and the IPv6 variants. Full coverage in X-Forwarded-For SQL injection.

Header: Host and X-Forwarded-Host

Multi-tenant SaaS apps route by hostname: acme.app.example and globex.app.example resolve to the same servers, and the app reads Host (or X-Forwarded-Host behind a proxy) to look up which tenant. If the tenant lookup query concatenates the host header, you have SQL injection in the hostname itself. Niche but real. See Host header SQL injection.

Session cookies are almost always safe in modern apps because the framework parses them. The exposure is the OTHER cookies: language preference, A/B-test bucket, theme, region, view count. Anything the app sets as a cookie AND reads back into a query is in scope. The lookup pattern SELECT theme_css FROM themes WHERE theme_id = '${cookie}' is the recurring bug. Detailed treatment in Cookie SQL injection.

Header: Authorization

Standard Authorization: Bearer <jwt> schemes parse the JWT, validate it, and lookup the user by the validated claim. That path is usually safe because the validation step constructs a typed identifier. The dangerous path is custom token schemes: Authorization: APIKey <key> where the app does SELECT user_id FROM api_keys WHERE token = '${key}'. Hand-rolled API key tables are over-represented in real-world findings. See Authorization header SQL injection.

Header: Accept-Language

Some apps write the requested language to the user's profile (UPDATE users SET locale = ? WHERE id = ?). Some construct a query to find translations matching the requested locale. If either path is hand-rolled rather than parameterised, the standard concatenation bug applies. Lower volume than the other headers, but still worth testing at --level=5 in sqlmap.

Header: custom (X-Api-Key, X-Tenant-Id, X-Request-Id)

Anything the application reads from a custom header. Common patterns:

  • X-Api-Key, same shape as Authorization-as-API-key.
  • X-Tenant-Id, same shape as Host header (multi-tenant lookup).
  • X-Request-Id / X-Correlation-Id, echoed into the audit log; same shape as User-Agent.
  • X-Original-URL, X-Rewrite-URL, used by some frameworks for internal routing; rarely a SQLi target but a common vector for other bugs (path bypass).

Test custom headers explicitly with sqlmap's --headers='X-Tenant-Id: 1*' syntax (the * marks the injection point).

Other: HTTP method

req.method becoming part of a logged audit entry: INSERT INTO audit (method, path, user_id) VALUES ('${method}', ?, ?). The method itself is attacker-controlled (you can send PROPFIND or WHATEVER or even arbitrary bytes if the HTTP parser is lax). Rare in modern stacks because frameworks normalise the method, but the pattern shows up in custom audit code.

How do you actually test all of these vectors?

Three rules of thumb:

  1. Default sqlmap misses headers. --level=1 (default) only tests query string and body. --level=3 adds User-Agent and Referer. --level=5 adds Host and most other common headers. Anything beyond requires explicit --headers='Header-Name: ...*' with a * injection-point marker.
  2. Test custom headers explicitly. sqlmap does not know what custom headers your app reads. Read the source, identify the headers the app actually consumes, add them as explicit injection points.
  3. Test all three injection contexts per vector. Numeric (no quotes), string single-quoted, string double-quoted. The same field may be vulnerable in one context and not another.

For the by-hand version, curl -H is the cleanest tool:

bash
# Test User-Agent for boolean-style SQLi
curl -H "User-Agent: 1' OR '1'='1" "https://target.example/page"

# Test X-Forwarded-For for time-based SQLi
curl -H "X-Forwarded-For: 1' AND SLEEP(5)-- -" "https://target.example/page"

Watch the response time, response body, or response code. Anything that differs from the baseline is signal.

Defender's checklist

The vector-agnostic version of the SQLi defence rule: every value the application reads from the HTTP request is untrusted, regardless of where it came from in the request. Concretely:

  • Parameterise every query, no exceptions, including logging and audit queries.
  • Audit logging code first. Audit and analytics are the most under-reviewed code paths in nearly every codebase, and they are the most likely to handle every header the app sees.
  • Apply a maximum length to every header before storage. Will not "fix" SQLi but bounds the blast radius dramatically: a SQLi exploit needs more than 200 bytes of payload for anything interesting, and most legitimate headers are under that.
  • Consider an HTTP allow-list at the edge: WAF rules that drop requests with payload-shaped headers (UNION, SLEEP, single quotes in unexpected places). Defence in depth, not the primary control.
  • Log enough to detect: any application database user that suddenly touches information_schema is the highest-signal SQLi detection rule in any codebase.

Where to go next

Per-vector deep dives:

And the rest of the cluster:

Sources

Authoritative references this article was fact-checked against.

TagsSQL InjectionHTTP HeadersWeb SecurityPenetration TestingOWASPApplication Security

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

SQL Injection in JSON Request Bodies (REST and GraphQL APIs)

JSON-body SQL injection is the modern face of the bug: REST APIs, GraphQL resolvers, and ORM raw-query escape hatches. How developers paint themselves into the corner with template strings around JSON fields, the manual exploit, sqlmap commands with --data and *, and the defence.

XXEinjector Cheat Sheet: Every Flag I Actually Use

A field reference for XXEinjector: target options, request file format with the XXEINJECT marker, OOB and direct modes, PHP filter wrappers, file enumeration, logging, and custom listeners. Grouped by what you are trying to do.