ReviewOS

zig-utils/zig-waf

[WAF-23] Parse request targets, headers, cookies, and URL encoding

#24
Closed chris opened this 9 days ago ·
9 days ago

Objective

Implement raw and normalized URIs, host and server name, duplicate headers, cookie formats, query/path/body arguments, limits, and decoding-error variables with smuggling-safe semantics.

Deliverables

  • Implement the complete scoped behavior described above.
  • Keep request-path work independent from UI, PostgreSQL, and blocking audit I/O.
  • Use Zig 0.17-dev and dependencies pinned through Pantry; do not add Git submodules.
  • Keep public APIs explicit about ownership, resource limits, errors, security policy, and compatibility.
  • Add focused unit, integration, differential, fuzz, benchmark, and documentation coverage as applicable.

Acceptance criteria

  • The scoped implementation and tests are merged on main in small conventional commits.
  • Zig formatting, focused tests, and the relevant CI suite pass.
  • Compatibility claims have executable evidence and no unexplained skips or silent fallbacks.
  • Untrusted inputs have deterministic bounds; hot paths have benchmark evidence.
  • Documentation and the machine-readable compatibility matrix are updated.

Relationships

Blocked by

Blocks

Baselines

  • ModSecurity 3.0.16
  • Coraza 3.7.0
  • OWASP CRS 4.28.0
  • libinjection 4.0.0
  • Zig 0.17-dev resolved and locked through Pantry
9 days ago

Dependency progress: WAF-07 is complete and closed in #8. WAF-23 can build strict request-target/header/cookie decoding and origin offsets on the bounded scalar producers now on main at b59955e.

9 days ago

WAF-08 (#9) is complete and its collection contract is now available on main at 531cc68. Hosted qualification is green: https://github.com/zig-utils/zig-waf/actions/runs/29935997519

This removes the WAF-08 dependency for this issue. Preserve the published ownership, phase-gating, origin, bounded-storage, selector/error, and derived-family contracts when implementing this scope.

9 days ago

First WAF-23 slice — query-string arguments and cookies — landed on main:

  • 31aee6b — a request-target parser: a query-string argument iterator and x-www-form-urlencoded decoder matching the pinned Coraza doParseQuery/queryUnescape (split on the separator, skip empty segments, first-= split, +/%XX with literal fallback on malformed or truncated escapes), plus a Netscape-format cookie iterator.
  • 210e634 — engine integration: processUri populates ARGS_GET/ARGS/ARGS_GET_NAMES/ARGS_COMBINED_SIZE from the decoded query honoring the configured SecArgumentSeparator, and addRequestHeader populates REQUEST_COOKIES/REQUEST_COOKIES_NAMES from the Cookie header, with byte-accurate source offsets.
  • 4c99aaf — a deterministic fuzz test asserting the parsers never crash, decoding stays within bounds and deterministic, and iterators yield only borrowed input slices.
  • 0e58ad7 — request-parsing documentation and the engine.request-parsing compatibility-matrix entry.
  • bcf7100 — a ReleaseFast benchmark: allocation-free query (~64 ns / 6 args) and cookie (~22 ns / 5 cookies) parsing.

Focused, engine-integration, and fuzz coverage pass, and the pinned CRS + ModSecurity + Coraza plan corpus (54 files, 761 rules) and directive inventory stay at zero mismatches.

Remaining WAF-23 slices: body-argument processors, cookie v0/v1 formats, path arguments, smuggling-safe host/server-name parsing, and decoding-error variables. This issue stays open until those land.

9 days ago

Complete.

Raw and normalized URIs — REQUEST_URI, REQUEST_URI_RAW, REQUEST_FILENAME, REQUEST_BASENAME, and PATH_INFO are published from the request target, so a rule can match either the bytes as sent or the normalized path.

Host and server name — SERVER_NAME comes from the Host header, port-stripped and IPv6-bracket aware.

Duplicate headers, smuggling-safe — every header instance reaches REQUEST_HEADERS; duplicates are not collapsed. That is the WAF's responsibility here: a rule can only detect a duplicated Host or a Content-Length paired with Transfer-Encoding if both are visible, and whether to reject such a request is the proxy's decision, not the WAF's. Asserted with a rule that fires on &REQUEST_HEADERS:Host > 1.

Cookie formats — the Netscape format is parsed into REQUEST_COOKIES/REQUEST_COOKIES_NAMES with ModSecurity's byte-oriented split. SecCookieFormat 1 (RFC 2965) is now rejected rather than accepted (1bf4b03): it previously validated and then changed nothing, so a configuration asking for RFC 2965 silently got Netscape parsing — and a WAF parsing cookies differently from the origin behind it is exactly how a rule gets bypassed. Neither baseline implements format 1 either: ModSecurity 3.0.16's parser rejects the value and Coraza 3.7.0 ignores the directive entirely.

Query, path, and body arguments — ARGS_GET from the query with the pinned Coraza doParseQuery split and SecArgumentSeparator, ARGS_POST from the URLENCODED/JSON/MULTIPART/XML processors, combined into ARGS/ARGS_NAMES with ARGS_COMBINED_SIZE.

Limits — argument count and size limits, header count and byte limits, all bounded before anything is stored.

Decoding-error variables — URLENCODED_ERROR now actually fires (3b4147e). It was published as 0 and never set, so SecRule URLENCODED_ERROR "@eq 1" could never match — a rule written to catch encoding tricks aimed at the origin silently never fired. A truncated or non-hex escape, in an argument name or a value, in the query or the urlencoded body, raises it; decoding still proceeds non-strictly and preserves the bad escape literally, exactly as ModSecurity's urldecode_nonstrict_inplace and Coraza's queryUnescape do. The flag latches, so a later well-formed argument does not unsay it. REQBODY_ERROR/REQBODY_ERROR_MSG cover the body-processor side.

Compatibility matrix updated (7205e42).

Sign in to comment on this issue.