Skip to content

feat: fuzz testing for the engine and all adapters - #62

Merged
deblasis merged 5 commits into
mainfrom
feat/fuzz-testing
Aug 17, 2026
Merged

feat: fuzz testing for the engine and all adapters#62
deblasis merged 5 commits into
mainfrom
feat/fuzz-testing

Conversation

@deblasis

Copy link
Copy Markdown
Contributor

What

Fuzz testing across the app and the adapters, guarding one invariant: client input never produces a 5xx. Starlark has no try/except, so any builtin raise on attacker-shaped input is an unhandled 500 — real APIs answer bad input with 4xx.

The harnesses

  • TestAdapterInputSafety — every reference adapter's routes, driven with deterministic adversarial requests: garbage path params (negative / huge / unicode), JSON-null and malformed bodies, batch arrays, bracket-form bodies, garbage auth, and every plausible cursor/limit query param poisoned simultaneously.
  • Go-native fuzz targetsFuzzMatchRoute (router), FuzzParseFormBody (bracket-form parser), FuzzParseMultipart (multipart decoder totality), FuzzValidateHeader (webhook header injection), and FuzzAdapterRequests: coverage-guided fuzzing through the full dispatch path over a curated adapter set (stripe, cloudflare-D1, salesforce-SOQL, powerplatform-OData, emailoctopus, eth-jsonrpc, shopify).
  • Seed corpora run in plain go test forever; just fuzz runs guided rounds locally; discovered inputs are committed under testdata/fuzz/.

First-run findings — all fixed

Finding Fix
paginate raised on invalid cursors — 10 adapters 500'd on a tampered token Returns (None, None); each adapter answers its provider's real 400 (Invalid pageToken, InvalidQueryParameterValue, …)
crypto.base64_decode/base64url_decode raised on malformed input (auth material, cursors) Total — None, never a raise (json_safe_decode's contract)
query_select/paginate raised on out-of-int64 limits Clamped — a huge limit means "no limit"
Bracket parser panicked on a[0]=v (terminal numeric index — the Rails array-literal form) Terminal-index branch + empty-segs guard; matrix cases added
Bracket parser took 57s on a[222222220][b]=v (222M-element sparse slice) Index capped at 10000; pathological pairs skipped
Router captured an empty param name for a {} manifest typo Never matches
eth-jsonrpc/erc4337 crashed on non-object batch elements / non-string method Per-element -32600 Invalid Request per the JSON-RPC 2.0 spec
anaplan built blob names from raw path params Identifiers validated → 400
printify/jira parsed unbounded ints from client ids (int64 overflow in the response converter) int64-bounded parsers

The bracket-parser panic and the 57s input are the standouts: both reachable by any client POSTing a urlencoded body to any form-accepting endpoint, both shipped in v0.41.0, and neither was caught by the existing suites — exactly what this harness exists for.

Test

  • Full suite, parse guard, QC boot, adapter lint, gofmt, vet — green.
  • Post-fix guided rounds: FuzzAdapterRequests 5min, FuzzParseFormBody 90s, FuzzParseMultipart 90s, FuzzMatchRoute 60s, FuzzValidateHeader 45s (768k execs) — all clean.

Go-native fuzz targets plus a deterministic all-adapter safety sweep
guarding the one invariant a mock must uphold: client input never
produces a 5xx. Starlark has no try/except, so any builtin raise on
attacker-shaped input is an unhandled 500; real APIs answer bad input
with 4xx.

- TestAdapterInputSafety: every reference adapter's routes driven with
  garbage params (negative/huge/unicode), JSON-null and malformed
  bodies, batch arrays, bracket-form bodies, garbage auth, and every
  plausible cursor/limit param poisoned at once.
- Fuzz targets: matchRoute (router), parseFormBody (bracket parser),
  parse_multipart (total contract), webhook header validation, and
  FuzzAdapterRequests — coverage-guided full-dispatch fuzzing over a
  curated adapter set (stripe, cloudflare D1, salesforce SOQL,
  powerplatform OData, emailoctopus, eth-jsonrpc, shopify). 5+ minutes
  clean on the adapter target after fixes.
- Seed corpora run in plain go test forever; just fuzz runs guided
  rounds; found inputs are committed under testdata/fuzz/.

First-run findings, all fixed:
- paginate raised on invalid cursors — 10 adapters 500'd on a tampered
  token. Now returns (None, None); each adapter answers its provider's
  real 400 (Invalid pageToken, InvalidQueryParameterValue, ...).
- crypto.base64_decode/base64url_decode raised on malformed input (auth
  material, cursors). Now total — None, never a raise.
- query_select/paginate raised on out-of-int64 limits. Now clamped: a
  huge limit means no limit.
- Bracket parser: a[0]=v (terminal numeric index, the Rails array
  literal) PANICKED on an empty recursion tail; a[222222220][b]=v
  materialized a 222M-element slice (57s). Fixed + matrix cases added.
- Router: a {} manifest typo captured an empty param name; never
  matches now.
- eth-jsonrpc/erc4337: non-object batch elements and a non-string
  method crashed dispatch; now per-element -32600 per the JSON-RPC spec.
- anaplan built blob names from raw path params; identifiers now
  validated (400).
- printify/jira parsed unbounded ints from client ids/params (int64
  overflow in the response converter); now bounded.
Review findings on PR #62, all fixed:

- CRITICAL: start+limit overflow — a huge limit (clamped to MaxInt64)
  plus a VALID cursor wrapped end negative and panicked the slice, in
  both paginate and query_select. Both now clamp against the remaining
  items; unit test pins cursor=1 + MaxInt64 limit. The paginate cursor
  is also digit-strict now (ParseInt accepted '+5').
- The (None, None) cursor guards covered only ~10 of the cursor-exposing
  adapters; the rest returned wrong 200s with null data or still 500'd
  (len(None)). 98 guards inserted across 40 more adapters, each using
  its provider's 400 envelope (Slack invalid_cursor, Shopify page_info,
  Dropbox invalid_cursor, Square INVALID_CURSOR, Google Invalid
  pageToken, Azure InvalidQueryParameterValue, ...).
- garbageQuery now poisons ~30 param names incl. case-sensitive and
  provider-specific ones (pagination_token, PageToken, nextPageToken,
  page_info, sysparm_offset, $skip/$skipToken, continuation-token,
  Marker, start, max_results, top, max-keys) — the sweep previously
  could not see most cursor paths.
- FuzzAdapterRequests now fuzzes the QUERY STRING too (path encoding
  previously swallowed '?', making every cursor/limit path unreachable
  by the fuzzer); seeds include tampered cursors and huge limits.
- fuzzServerFor closes engines + removes state dirs in plain go test
  seed runs (only -fuzz workers keep them for the process lifetime).
- smartbill comment updated to the total base64_decode contract.

Gates: full suite, parse guard, QC boot, adapter lint, gofmt, vet green;
FuzzAdapterRequests 4min with queries clean; all-adapter sweep green on
the expanded poison set.
@deblasis
deblasis merged commit d895c18 into main Aug 17, 2026
1 check passed
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.

1 participant