Skip to content

fix: Cloudflare keyed header/cookie conditions emit invalid wirefilter - #276

Closed
gfargo wants to merge 1 commit into
mainfrom
fix/cloudflare-keyed-header-cookie-conditions
Closed

fix: Cloudflare keyed header/cookie conditions emit invalid wirefilter#276
gfargo wants to merge 1 commit into
mainfrom
fix/cloudflare-keyed-header-cookie-conditions

Conversation

@gfargo

@gfargo gfargo commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Fixes #269

Summary

A keyed header or cookie condition (e.g. "match this specific header/cookie name") compiled to wirefilter Cloudflare's real API rejects — found while researching #263's query fix, which surfaced that the existing header/cookie key-scoping code shares (and in cookie's case, exceeds) the same construct problem query had.

  • header: compiled to http.request.headers["key"] eq "value". http.request.headers is Map<Array<String>> — indexing it yields an Array, and comparing an Array against a String literal via eq is a type mismatch. Verified against Cloudflare's own "Require specific headers" WAF guide, which uses any(http.request.headers["key"][*] eq "value") for exactly this case.
  • cookie: worse — compiled to http.cookie["key"] eq "value", but http.cookie is a scalar String (the whole Cookie header), not a Map at all. Verified Cloudflare has a separate http.request.cookies field that is the right Map<Array<String>> shape (same any(...) idiom), so the fix points the keyed case at that field instead, leaving the scalar http.cookie untouched for the unkeyed case.
  • Header key casing: also lowercases the header key now — Cloudflare's docs state header map keys are always lowercase, so a condition built with key: 'Content-Type' would previously have silently never matched a real request (this was true before this PR too, on the old bracket-index shape). Cookie keys are left as-is — Cloudflare doesn't case-normalize those.

exists/not_exists for both now use has_key(map, "key"), matching the idiom Cloudflare's own changelog documents for map-key existence checks (same as #263's query fix already used).

Scope notes

  • ExpressionBuilder.fromVercelCondition/FieldMapper (the direct Vercel-native → Cloudflare path, not the unified-config path this PR fixes) has the identical bug, plus an unfixed keyed-query case of its own — but it's dead code, unreachable from any live command (confirmed via grep). Left alone with a code comment explaining why and what to fix first if it's ever wired back in.
  • A keyed header/cookie condition no longer round-trips through WirefilterParser (it doesn't parse any(...)/has_key(...) syntax) — this is a deliberate, documented degradation matching the parser's own existing contract (unsupported construct → null, caller warns), not a silent misparse. Extending the parser to understand the new construct is real follow-up work, intentionally not bundled into this fix.

Test plan

  • Regression tests for header (value comparison, exists, not_contains, negation, case-lowercasing) and cookie (value comparison, exists, not_in, case-preservation), each set mutation-verified
  • Separately mutation-verified the case-normalization logic on its own (a distinct, easy-to-silently-break behavior from the core Array-vs-String fix)
  • pnpm compile && pnpm test && pnpm lint all pass (1764 tests, 0 lint errors)

A keyed header condition (e.g. matching a specific Content-Type) compiled
to a bare `http.request.headers["key"] eq "value"` — but that field is
Map<Array<String>>, so indexing it yields an Array, and comparing an Array
against a String literal is a type mismatch the Cloudflare API rejects.
Keyed cookie conditions were worse: `http.cookie["key"]` brackets-indexes
a plain scalar String field that isn't a Map at all.

Verified against Cloudflare's Ruleset Engine docs (both fields' real
types, and the "Require specific headers" WAF guide's actual idiom) rather
than assuming:
- header: any(http.request.headers["key"][*] <op> value) / has_key(...)
  for exists, same construct #263 already used to fix keyed query
  conditions. Also lowercases the key — Cloudflare's header map keys are
  documented as always-lowercase, so a mixed-case key would silently never
  match.
- cookie: same any()/has_key() construct, but pointed at the separate
  http.request.cookies map field instead of the scalar http.cookie (which
  stays as-is for the *unkeyed* case). Cookie keys are NOT lowercased —
  left in their original casing.

fromVercelCondition/FieldMapper (the direct Vercel-native -> Cloudflare
path) has the same bug plus an unfixed keyed-query case, but is unreachable
from any live command as of this change — left alone with a comment
explaining why, since fixing dead code protects no one.

A keyed header condition no longer round-trips through WirefilterParser
(it doesn't understand any()/has_key() syntax) — a deliberate, documented
degradation matching the parser's existing "unsupported construct ->
null, caller warns" contract, not a silent misparse. Extending the parser
for the new syntax is tracked separately.
@gfargo

gfargo commented Aug 24, 2026

Copy link
Copy Markdown
Owner Author

Closing without merging — #278 landed first (merged as c529e57, ~seconds before this PR's CI finished) and independently arrived at the same fix via the same research (same any(field["key"][*] <op> value)/has_key(...) idiom, same buildKeyedMapExpression helper name), but went further than this PR did:

No action needed here beyond closing — see #278 for the real fix.

@gfargo gfargo closed this Aug 24, 2026
@gfargo
gfargo deleted the fix/cloudflare-keyed-header-cookie-conditions branch August 24, 2026 20:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cloudflare keyed header/cookie conditions likely emit wirefilter the API rejects (Array-vs-String, and indexing a String field)

1 participant