Skip to content

fix(adapters): JSON-null required-field bypass — 109 sites across 65 adapters - #63

Merged
deblasis merged 2 commits into
mainfrom
feat/null-guard-hardening
Aug 17, 2026
Merged

fix(adapters): JSON-null required-field bypass — 109 sites across 65 adapters#63
deblasis merged 2 commits into
mainfrom
feat/null-guard-hardening

Conversation

@deblasis

Copy link
Copy Markdown
Contributor

What

Closes the guard-integrity audit (stunt-qi0) — the one class the v0.45.0 fuzz invariant cannot see: a JSON null in a required field produces a wrong 200 with a stored null, not a 500.

Starlark decodes JSON null to None, and None == "" is False — so V = body.get(k, "") + if V == "": return 400 passes a {"username": null} body. The null gets stored and echoed where every real API rejects the request.

Fix

V = body.get(k, "")V = body.get(k) or "" — applied only at extraction+guard pairs where the guard (or its 3-line window) lacked its own == None / != None / type() check. Sites that already null-check (e.g. if x == None or x == "") were measured and left alone. Null now lands as "" and each provider's existing required-field 400 fires with its real error shape.

109 sites, 65 adapters — OAuth client-id/secret checks, signup/credential fields (Cognito, Firebase), create-required names/urls (Cloudflare zones/D1/R2, Slack, Jira, Shopify, Stripe subscriptions, Onfido, Persona, PSD2 consents, …).

Test

  • TestAWSCognitoStyleAdapter pins it: SignUp with Username: null → 400 InvalidParameterException (an auth-free endpoint, so the pin exercises exactly this guard).
  • Full suite, parse guard, QC boot, adapter lint, the all-adapter input-safety sweep, gofmt, vet — green. (TestDownEndToEndWithRealBinary flaked once under post-fuzz machine load; 2x green in isolation.)

…adapters

Starlark decodes JSON null to None and None == "" is False, so a
plain .get(k, "") + == "" required-field guard PASSES a null field:
{"username": null} was stored and echoed as null where every real API
rejects it (the guard-integrity audit stunt-qi0, the one class the
never-5xx fuzz invariant cannot see — it manifests as a wrong 200).

Fix at the extraction: V = body.get(k, "") -> V = body.get(k) or ""
at the 109 paired extraction+guard sites where the guard lacked its own
None/type check (sites already checking == None or type(V) != "string"
were left alone). Null now lands as "" and the existing required-field
400s fire with each provider's real error.

Pinned by TestAWSCognitoStyleAdapter: SignUp with Username: null ->
400 InvalidParameterException.
Review findings on PR #63:

- salesforce upsert-insert (sobjects.star:316) and composite
  (composite.star:226): inline 'body.get("Name", "") == ""' guards
  passed {"Name": null} on the upsert path while the plain-create path
  (fixed in the first commit) rejects it — create and upsert now agree.
- powerplatform dataverse create (dataverse.star:207): a null accountid
  skipped id autogeneration and then crashed building the Location
  header (string + None) instead of returning 201-with-generated-id.
- stripe subscriptions discount guard: same permeable inline shape,
  contrived-only impact, fixed for consistency.
- square refunds (refunds.star): a null payment_id crashed the 404
  error path itself ('Payment ' + None) — coerced at extraction.
@deblasis
deblasis merged commit c77e539 into main Aug 17, 2026
1 of 3 checks passed
@deblasis
deblasis deleted the feat/null-guard-hardening branch August 18, 2026 10:41
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