Skip to content

feat(scan): --path-traversal mutator for OWASP A01 scope-escape (../etc/passwd, encoded, double-encoded, nested, null-byte, absolute)#30

Merged
bugsyhewitt merged 1 commit into
mainfrom
worker-r33-possession
May 30, 2026
Merged

feat(scan): --path-traversal mutator for OWASP A01 scope-escape (../etc/passwd, encoded, double-encoded, nested, null-byte, absolute)#30
bugsyhewitt merged 1 commit into
mainfrom
worker-r33-possession

Conversation

@bugsyhewitt

Copy link
Copy Markdown
Owner

Summary

Adds the --path-traversal mutator — a focused Phase 2 improvement that closes
a real gap in possession's authz-bypass coverage. The operator's task allowed
either --ssrf-probe or --path-traversal; I verified neither was present
(the 17 existing mutators in internal/mutate/ and the 17 flags in
internal/cli/scan.go cover identity/object/forbidden-bypass/host/cookie/
header-injection/param-pollution/origin-spoof/ct-confusion/cache-deception/
prototype-pollution/xxe/graphql/mass-assign/method-override/csrf-header/
ws-hijack — no traversal or SSRF). Picked --path-traversal because it
fits possession's BOLA/IDOR niche cleanly (it is a resource-scope-escape
mutator, the same vocabulary as --swap-object / --enumerate /
--forbidden-bypass); SSRF is a different niche.

What it does

For each eligible captured request (any path beyond /), emits 18
deterministic variants — 6 techniques × 3 high-signal targets — that
replace the trailing path segment with a traversal payload:

Technique Wire-form payload
dot-dot-slash ../../../../../../etc/passwd
dot-dot-encoded ..%2f..%2f...etc/passwd (RawPath keeps %2f)
dot-dot-double-encoded ..%252f..%252f...etc/passwd
nested-dot-dot ....//....//...etc/passwd
null-byte-suffix ../../...etc/passwd%00
absolute-path /etc/passwd (no .. at all)

Targets: etc/passwd, proc/self/environ, windows/win.ini.

Findings class authz-bypass (ASVS V12.3). Caller credentials are
preserved on every variant — the bug being tested is "same caller
breaks out of the route prefix." Root paths emit no variants
(no segment to reshape).

Disjointness

  • vs --forbidden-bypass (traversal-semicolon): that reshapes the
    path to resolve back to the same handler (/admin/..;/admin);
    this escapes the resource subtree entirely.
  • vs --swap-object / --enumerate: those stay inside the resource
    collection (substitute / sweep known IDs); this breaks out of it.

Wiring

Always registered in buildRegistry (so the canonical order /
allowlist contract is stable); inert when --path-traversal is not
passed. Mirrors the gating pattern every other off-by-default mutator
uses (--cache-deception, --prototype-pollution, etc.).

Tests

  • New internal/mutate/path_traversal_test.go (18 tests): disabled-by-default,
    nil-safety, root-skip, credential preservation, full cross-product count,
    per-technique wire-form assertions (Path + RawPath + URL.String invariants
    for %2f / %252f / %00), determinism, sorted emission, disjointness from
    forbidden-bypass, Name() stability, NotInDefaultRegistry, degenerate-input
    guards.
  • internal/cli/buildregistry_forbidden_test.go: added TestBuildRegistry_PathTraversalGating
    • TestBuildRegistry_PathTraversalWithWordlist mirroring the cache-deception /
      prototype-pollution gating tests. Mechanically appended trailing , false
      to every other buildRegistry() test call for the new signature parameter.

Full suite (go test ./...) and go vet ./... pass clean.

README

New ## Directory / path traversal (--path-traversal) section after
the prototype-pollution section, with the technique table and the
disjointness paragraph against --forbidden-bypass / --swap-object.

Test plan

  • go build ./... clean
  • go vet ./... clean
  • go test ./... — all packages green
  • possession scan --help shows the new --path-traversal flag
  • Manual smoke against a corpus app with a per-user file endpoint
    (operator follow-up — outside Worker scope)

🤖 Generated with Claude Code

…d/nested/null-byte/absolute payloads against etc/passwd, proc/self/environ, win.ini for OWASP A01 path-traversal / LFI

Adds the PathTraversal mutator (off by default behind --path-traversal).
Disjoint from --forbidden-bypass (which reshapes the path to resolve back
to the SAME handler, e.g. /admin/..;/admin) and from --swap-object /
--enumerate (which stay inside the resource collection): path-traversal
escapes OUT of the resource subtree the route prefix was supposed to
confine the caller to, reaching OS-sensitive files or sibling-tenant
directories.

Six techniques × three targets = 18 deterministic variants per eligible
endpoint, emitted in sorted-by-technique then sorted-by-target order:

  - dot-dot-slash:           ../../../../../../etc/passwd
  - dot-dot-encoded:         ..%2f..%2f...etc/passwd  (RawPath survives)
  - dot-dot-double-encoded:  ..%252f..%252f...etc/passwd
  - nested-dot-dot:          ....//....//...etc/passwd
  - null-byte-suffix:        ../../...etc/passwd%00
  - absolute-path:           /etc/passwd   (no `..` at all)

Targets: etc/passwd, proc/self/environ, windows/win.ini.

Every variant keeps the caller's own credentials (Identity == nil) — the
bug being tested is "the same legitimately-credentialed caller breaks
out of the resource subtree." Root / empty paths emit no variants
(no trailing segment to reshape). Findings class authz-bypass.

Wired into buildRegistry as a always-registered, opt-in mutator
(mirrors the cache-deception / prototype-pollution gating pattern).
@bugsyhewitt bugsyhewitt merged commit 604b380 into main May 30, 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