Skip to content

feat(corpus): XXE, path-traversal and Go SSRF cases + pinned real repos - #31

Merged
bkd-dotcom merged 1 commit into
mainfrom
feat/corpus-xxe-traversal-ssrf
Aug 18, 2026
Merged

feat(corpus): XXE, path-traversal and Go SSRF cases + pinned real repos#31
bkd-dotcom merged 1 commit into
mainfrom
feat/corpus-xxe-traversal-ssrf

Conversation

@bkd-dotcom

Copy link
Copy Markdown
Member

Closes #11, #12, #29 and #13.

Corpus goes 52 → 60 cases.

case class language issue
LANG-53-java-xxe XXE (CWE-611) Java #11
LANG-54-php-xxe XXE (CWE-611) PHP #11
LANG-56-go-path-traversal traversal (CWE-22) Go #12
LANG-57-java-path-traversal traversal (CWE-22) Java #12
LANG-59-go-ssrf SSRF (CWE-918) Go #29
LANG-55-SAFE-php-xml-default PHP decoy
LANG-58-SAFE-go-constant-path Go decoy
LANG-60-SAFE-go-constant-url Go decoy

The decoys are the point as much as the positives

Each probes a precision distinction the corresponding rule has to make:

  • LANG-55$doc->loadXML($_POST['xml']) with no entity flags. On PHP 8 / libxml 2.9 external entities are off by default, so parsing untrusted XML is not itself XXE. A rule that flags "parses XML" fails here.
  • LANG-58os.ReadFile("/etc/app/config.yaml"). A constant path is not traversal.
  • LANG-60 — a constant host with a user-supplied query string.

LANG-60 paid for itself immediately. It caught a false positive in signetry-core's brand-new go.taint.ssrf rule: recall was 1.0 but the decoy scored 1 FP, because the rule fired on any tainted identifier on the request line —

q := r.URL.Query().Get("q")
resp, _ := http.Get("https://api.example.com/search?q=" + url.QueryEscape(q))

— where the destination is compiled in. That's the same FP class the Python SSRF rule is held to (#89), so the Go rule had to meet it too. Fixed in Signetry/core#97 before this PR landed.

Real repos (#13)

Two cases, both pinned to a full SHA:

  • OWASP WebGoat (Java) — 7517acca…
  • OWASP RailsGoat (Ruby) — 0222f7da…

First JVM/Ruby targets here — every existing real-repo case is Python or JavaScript, which under-exercises the multi-language tier.

…and the pin mechanism was broken

Adding them surfaced that commit= did nothing. scan_real_repo clones with --depth 1, then ran git checkout <sha> with check=False. On a shallow clone the object isn't present, so:

fatal: unable to read tree 0222f7da...

…the failure was swallowed and the scan silently ran against the default-branch tip. A case documented as "pinned for reproducibility" was not pinned — which matters for a benchmark whose selling point is auditability.

Verified both behaviours against a real shallow clone of railsgoat:

OLD: git checkout <older-sha>          -> fatal: unable to read tree
NEW: git fetch --depth 1 origin <sha>  -> fetch ok
     git checkout FETCH_HEAD           -> HEAD == the pinned sha

If pinning genuinely can't be honoured, the note now says NOT pinned (… scanned default branch) rather than passing an unpinned scan off as pinned.

Dependency

Pins signetry-core at v0.7.0 (released today) — the new cases depend on its Go SSRF, Go/Java path-traversal and PHP XXE rules. Against the old v0.6.0 pin, 2 tests fail; I confirmed that before cutting the release rather than after.

Verification

Against the published v0.7.0 tag in a clean environment:

60 cases · recall 1.0 · 0 false positives
50 tests pass · ruff clean

5 new tests: case presence, multi-language coverage per class, detection of all five new positives, zero FP on all three new decoys, and that the real-repo pins are full 40-char SHAs with provenance.

Closes #11, #12, #29, #13.

Corpus goes 52 -> 60 cases:
  * LANG-53/54  XXE (CWE-611) in Java and PHP            (#11)
  * LANG-56/57  path traversal (CWE-22) in Go and Java   (#12)
  * LANG-59     SSRF (CWE-918) in Go                     (#29)
  * LANG-55/58/60  three SAFE decoys

The decoys are the point as much as the positives: default-safe PHP XML
parsing (entities are off by default on PHP 8+, so "parses XML" is not the
bug), a constant filesystem path, and a constant host with a user-supplied
query string.

LANG-60 paid for itself immediately — it caught a false positive in
signetry-core's brand-new go.taint.ssrf rule (constant host + tainted
query string reported as SSRF), fixed in Signetry/core#97 before this
landed. That is exactly the false-positive class the Python SSRF rule is
held to, so the Go rule had to meet it too.

Real repos (#13): OWASP WebGoat (Java) and OWASP RailsGoat (Ruby), both
pinned to a full SHA. They are the first JVM/Ruby targets here — every
existing case is Python or JavaScript, which under-exercises the
multi-language tier.

Fixes the pin mechanism while adding them. scan_real_repo cloned with
--depth 1 then ran `git checkout <sha>` with check=False. On a shallow
clone the object is absent, so the checkout failed with

    fatal: unable to read tree <sha>

the failure was swallowed, and the scan silently ran against the
default-branch tip — so a case documented as "pinned for reproducibility"
was not pinned. Verified both behaviours against a real shallow clone.
Now fetches the object first and, if pinning truly cannot be honoured,
records that in the note instead of passing an unpinned scan off as
pinned.

Requires signetry-core v0.7.0 (pin bumped): the new cases depend on its Go
SSRF / Go+Java path-traversal / PHP XXE rules. Verified against the
published tag in a clean environment — 60 cases, recall 1.0, 0 false
positives; 50 tests and ruff green.
@github-actions

Copy link
Copy Markdown

Signetry Reviewer — 🟡 Needs human review

A human should decide — the required check is unknown; 1 advisory finding(s) to weigh.

Deterministic gates (the authority)

Gate Status
Required status check — unknown
Secret scan ✅ clean
CI permission / OIDC ✅ no forbidden change
Dependency skew ✅ ok
All green

Findings (1, 0 blocking)

  • 🟡 Change touches a protected path: pyproject.toml pyproject.toml (via cross-check)
    • pyproject.toml matches a protected pattern (pyproject.toml). Changes here alter shared/foundational surface and warrant a designated reviewer.
    • Fix: Route to a code owner / architecture reviewer.

Merge

A human should review and merge.

This review is advisory. It never merges on its own judgement — the deterministic gates + a human are the authority. Findings can have false negatives; a green bot verdict is not a guarantee.

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.

Corpus: add XXE (CWE-611) cases (Java + PHP)

1 participant