Skip to content

feat(scan): --content-type-confusion relabels Content-Type to bypass parser-dispatch authz#27

Merged
bugsyhewitt merged 1 commit into
mainfrom
worker-r30-possession
May 29, 2026
Merged

feat(scan): --content-type-confusion relabels Content-Type to bypass parser-dispatch authz#27
bugsyhewitt merged 1 commit into
mainfrom
worker-r30-possession

Conversation

@bugsyhewitt

Copy link
Copy Markdown
Owner

Summary

Adds --content-type-confusion, a new access-control bypass mutator that keeps the request body byte-identical and relabels only the Content-Type header. The bug being tested: a fronting WAF / API gateway short-circuits its body-inspection rules ("this is text/plain, no JSON to validate") while the handler still parses the body as JSON, or the handler is wired to multiple parsers (JSON / XML / form) with different authz middleware and is coerced onto the alternate path.

Where --parameter-pollution attacks which copy of a duplicated parameter each layer reads, --host-header attacks which host the access-control layer believes the request targets, and --xxe attacks how the XML parser resolves entities, --content-type-confusion attacks which body parser each layer of the stack chooses — the canonical Content-Type confusion / parser-sniffing family.

What's shipped

  • internal/mutate/content_type_confusion.go — pure / deterministic mutator. Three technique sets keyed on body shape:
    • JSON (declared *json* or sniffed { / [) → as-form, as-text, as-xml, strip-type (drop the header entirely).
    • XML (declared *xml* or sniffed <?xml / leading tag) → as-json, as-text.
    • urlencoded formas-json only (forms are too easy to mis-sniff for a wider set).
  • Every variant keeps the caller's credentials (Identity == nil) and the body bytes — only Content-Type changes. Class authz-bypass (ASVS V8.3.x, severity HIGH).
  • No-op relabels are skipped (declared type already matches target, parameter-insensitive — application/json; charset=utf-8 == application/json) so the comparative ladder always sees a real probe.
  • Binary, multipart, empty bodies produce no variants.
  • Wired through buildRegistry as a 16th gating bool; registered (inert when disabled) so the canonical DefaultRegistry order is unchanged. Off by default.
  • README + CHANGELOG updated.

Test plan

  • go test ./internal/mutate/ -run ContentType -race -count=1 — all 14 new unit tests pass (disabled default, nil/empty/unrecognised inputs, credential+body preservation, exact technique set per body shape, strip-type header drop semantics, no-op skipping with and without charset parameters, body-sniffing when Content-Type is absent, deterministic order, name).
  • go test ./internal/cli/ -run ContentType -race -count=1 — both buildRegistry gating tests pass (default and wordlist construction paths).
  • go test ./... -race -count=1 — full suite green: 510 tests across 14 packages, ok.
  • go vet ./... — clean.
  • go build ./... — clean.

POST_V01 candidate: Content-Type confusion mutator.

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

…parser-dispatch authz

Where --parameter-pollution attacks which copy of a duplicated parameter each
layer reads, --host-header attacks which host the access-control layer believes
the request targets, and --xxe attacks how the XML parser resolves entities,
--content-type-confusion attacks which body parser each layer of the stack
chooses. The body bytes are kept byte-identical; only the Content-Type header is
mutated. The bug being tested: a fronting WAF/gateway short-circuits its body
inspection rules ("this is text/plain, no JSON to validate") while the handler
still parses the body as JSON, or the handler is wired to multiple parsers
(JSON / XML / form) with different authz middleware and is coerced onto the
alternate path.

Every variant keeps the caller's credentials (Identity == nil) and the original
body bytes; only the Content-Type header (and the captured ContentType field)
change. Three deterministic technique sets, one per body shape:

  - JSON-shaped bodies (declared *json* or sniffed `{` / `[`):
    as-form, as-text, as-xml, strip-type (drop the header).
  - XML-shaped bodies (declared *xml* or sniffed `<?xml` / leading tag):
    as-json, as-text.
  - urlencoded form bodies: as-json only (the highest-signal mismatch; form
    bodies are too easy to mis-sniff for a wider set).

The media-type comparison is parameter-insensitive
(application/json; charset=utf-8 == application/json) so a no-op relabel
(declared type already matches target) is skipped. Binary, multipart, and
empty bodies produce no variants. Findings are class authz-bypass
(ASVS V8.3.x, severity HIGH).

Off by default; threaded through buildRegistry as a 16th bool, mirroring the
pattern of --origin-spoof, --parameter-pollution, --header-injection,
--cookie-tampering, --host-header, --forbidden-bypass, --method-override,
--csrf-header, --ws-hijack, --xxe, and --mass-assign. Registered (inert when
disabled) so the DefaultRegistry order is unchanged.

Includes 14 new unit tests covering the disabled default, nil/empty/unrecognised
inputs, credential+body preservation, the exact technique set per body shape,
header drop semantics for strip-type, no-op skipping with and without charset
parameters, body-sniffed classification when Content-Type is absent, and
deterministic output order; plus two buildRegistry gating tests (default and
wordlist construction paths). Full suite (-race) passes: 510 tests across 14
packages, ok.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bugsyhewitt bugsyhewitt merged commit 149621a into main May 29, 2026
2 checks 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