ReviewOS

zig-utils/zig-waf

[WAF-38] Add parser, body, ABI, HTTP, and storage fuzzing

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

Objective

Fuzz every untrusted parser and ABI, transformations, macros, persistent storage, audit formats, and HTTP framing with resource limits and sanitizer coverage.

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

None

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 WAF-10 #11 is complete at e72709c with hosted green evidence: https://github.com/zig-utils/zig-waf/actions/runs/29942330646. This issue is now unblocked on the bounded, owned SecLang frontend.

9 days ago

Complete for everything that runs in this repository; the HTTP-framing target belongs to the data plane.

Untrusted parsers — SecLang parsing and structural-plan compilation (fuzz-parser, fuzz-plan), typed directives (fuzz-directives), transformations (fuzz-transformations), non-disruptive actions (fuzz-actions), and operators (fuzz-operators), all deterministic with pinned seeds so a failure reproduces exactly.

Request bodies (598dcdc) — a new oracle over all four processors, with the corpus byte selecting which one reads the body, so a mutation can hand the same bytes to a different parser. It asserts what a violation would make a vulnerability: the transaction survives any body, the raw body stays readable whatever the processor made of it, the transaction is usable for the phases that follow, and nothing leaks. Two of this session's defects lived here — a JSON body that overflowed the stack by nesting, and one that published no arguments because of a duplicate key.

ABI (9e9961e) — arbitrary rule bytes were already fuzzed; the call order now is too. A data plane that processes a body before a URI, evaluates a phase twice, or asks for a decision at an unexpected moment must get a status back, never undefined behaviour. Those are the mistakes that surface in production rather than in a unit test.

Audit formats (a2593a7) — and this one found a real defect immediately. An audit record is built from bytes an attacker chose, and a request is under no obligation to be UTF-8 while a JSON string is; bytes above 0x7F were copied through unexamined, so a request containing a lone continuation byte produced a document no JSON parser would accept. An attacker could make the record of their own attack unreadable. Fixed by replacing ill-formed sequences with U+FFFD while preserving well-formed multi-byte text exactly. The oracle checks the structural property in each format rather than the text — JSON must parse as JSON, and a native record must not contain a boundary marker that splits it in two.

Macros — exercised through the plan oracle, which walks every compiled macro program and validates its token spans against the source registry.

Persistent storage (7be7a48) — collection keys and values come from the request and cross the backend boundary as keys. Fuzzed for what a backend author must be able to rely on: bounds enforced before storage, oversized values reported rather than truncated, and a rejected write leaving the transaction usable.

Resource limits — every oracle runs with deliberately tight limits, so cases reach the bounded paths (truncation, rejection, depth) rather than only the path where everything fits, and all run under the testing allocator, so a leak fails rather than accumulating.

One thing this uncovered about the harness itself: src/root.zig registers oracles for the test build through an explicit list, and the new ones were not in it — their unit tests compiled but never ran, passing by not existing. That is the same shape as the silently-ignored configuration this session has been removing, so it is fixed and noted rather than quietly corrected. The suite went from 438 to 442.

HTTP framing is not fuzzed here because this repository contains no HTTP parser: request framing lives in the data plane (#41), and fuzzing it belongs with that code.

CI runs the body (10,000), audit (5,000), and existing oracles on every push.

Sign in to comment on this issue.