Skip to content

fix(scan): require XML evidence in the file before reporting XXE at parse() - #86

Merged
ralyodio merged 1 commit into
fix/typosquat-scoped-packagesfrom
fix/xxe-requires-xml-evidence
Aug 10, 2026
Merged

fix(scan): require XML evidence in the file before reporting XXE at parse()#86
ralyodio merged 1 commit into
fix/typosquat-scoped-packagesfrom
fix/xxe-requires-xml-evidence

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Stacked on #85 — review that one first; this PR's diff is the second commit.

The bug

java-xxe-parse-call matches on the receiver's name:

pattern: /\b\w*(?:[Bb]uilder|[Pp]arser|[Rr]eader)\s*\.\s*parse\s*\(/

That shape is right for the vulnerability — real XXE is builder.parse(is), and the declared type is rarely on the same line — but a name suffix says nothing about XML. Every parser that parses something else matched too, at high severity under CWE-611.

On ionic-team/capacitor, all six hits were HostMask.Parser.parse(...), a hostname-mask parser:

file what it actually parses
Bridge.java:254, :1582 allowed-navigation hostnames
UriMatcher.java:148 a URI host mask
HostMaskTest.java:12–14 the string "*.example.org" in a unit test

Those three files contain zero references to javax.xml, org.xml.sax, org.w3c.dom, or any XML type.

The fix

Adds fileRequires, a precondition tested against the whole file rather than the guard window, and points the rule at the XML packages and types.

A file that parses XML says so at the top; one that never mentions XML is not parsing it. The existing requires could not express this — it searches a ±6-line window, and an import sits hundreds of lines from the match. Widening guardBack far enough to reach it would drag unrelated evidence into every other rule that shares the window.

The rule still fires on the shape it was written for — builder.parse(is) in a file that imports DocumentBuilderFactory — which is covered by a new positive test, so this narrows scope without giving up the detection.

Cost

The whole-file text is memoised on the lines array via a WeakMap, so the check costs one join per file rather than one per line. Without that, a file-level regex would make scanning quadratic in file length.

Verification

Against ionic-team/capacitor at 5e5bb3b: 19 findings → 13, removing all six java-xxe-parse-call reports and nothing else. Combined with #85, 21 → 13.

Three tests added. The two negative cases fail on master with java-xxe-parse-call firing; the positive case pins that a genuinely unhardened XML parse is still caught.

vitest run in apps/cli: 68 passed. tsc --noEmit: clean.

…arse()

`java-xxe-parse-call` matched on the receiver's *name* — anything suffixed
Builder, Parser or Reader calling `.parse(`. That shape is right for the
vulnerability, because the real XXE is `builder.parse(is)` and the declared
type is rarely on that line, but the suffix says nothing about XML. Any
parser that parses something else matched too, at high severity under
CWE-611.

On ionic-team/capacitor that meant `HostMask.Parser.parse(origins)` — a
hostname mask — reported three times in Bridge.java and UriMatcher.java,
plus three more in a unit test asserting on "*.example.org" strings.

Adds `fileRequires`, a precondition tested against the whole file rather
than the guard window, and points the rule at the XML packages and types.
A file that parses XML imports javax.xml or org.xml.sax at the top; one
that never mentions XML is not parsing it. The window could not answer
this — an import sits hundreds of lines from the match, and widening
guardBack far enough would drag unrelated evidence into every other rule.

The whole-file text is memoised on the lines array so the check costs one
join per file rather than one per line.

Verified against ionic-team/capacitor: 19 findings to 13, removing all six
java-xxe-parse-call reports and nothing else. Combined with the scoped
typosquat fix, 21 to 13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

144 finding(s)

HIGH/CRITICAL: 12 | MEDIUM: 96 | LOW: 36

Severity Rule Location
HIGH sql-template-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:30
HIGH secret-aws-access-key apps/cli/src/scan/secret-rules.ts:192
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:31
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:102
HIGH js-shell-exec-interpolation modules/code-scanner/src/__tests__/sast.test.ts:108
HIGH secret-aws-access-key modules/code-scanner/src/secrets/rules.ts:74
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
HIGH js-unsafe-yaml-load apps/cli/src/scan/__tests__/code-rules.test.ts:215
HIGH manifest-typosquat apps/mobile/package.json:43
HIGH secret-generic-credential modules/spend-guard/config/example.conf.toml:13
HIGH secret-generic-credential modules/spend-guard/README.md:84
HIGH secret-generic-credential PRD.md:268
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:70
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:79
MEDIUM sql-template-interpolation apps/cli/src/commands/properties.ts:226
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:88
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:111
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:121
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:125
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:31
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:33
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:34
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:35
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:36
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:43
MEDIUM sql-template-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:56
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:63
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:82
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:84
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:85
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:93
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:98
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:105
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:112
MEDIUM sql-template-interpolation apps/cli/src/index.ts:105
MEDIUM sql-template-interpolation apps/cli/src/index.ts:110
MEDIUM sql-template-interpolation apps/cli/src/index.ts:120
MEDIUM js-shell-exec-interpolation apps/cli/src/index.ts:411
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:20
MEDIUM sql-template-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:34
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:39
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:48
MEDIUM sql-string-concatenation apps/cli/src/scan/__tests__/code-rules.test.ts:52
MEDIUM js-shell-exec-interpolation apps/cli/src/scan/__tests__/code-rules.test.ts:64
MEDIUM js-unsafe-yaml-load apps/cli/src/scan/__tests__/code-rules.test.ts:211
MEDIUM sql-template-interpolation apps/extension/scripts/build.js:320
MEDIUM sql-template-interpolation apps/extension/scripts/build.js:326
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:180

…and 94 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 799e85b into fix/typosquat-scoped-packages Aug 10, 2026
4 checks passed
ralyodio added a commit that referenced this pull request Aug 10, 2026
…ge guard (#88)

Release 0.6.0.

Two languages the scanner claimed to support had no rules at all. `shell`
and `php` were both in `ScanLanguage`, both mapped from extensions, and
between them zero of the 46 code rules targeted either. From the outside
they looked supported: files were read, matched against the secret rules,
and reported clean whatever the code did.

Language detection made it worse — it was extension-only, so an executable
named for the command it provides rather than the language it is written in
was never opened. `ralyodio/debtap`, 3,511 lines of bash in a file called
`debtap`, scanned clean by scanning nothing and exited 0 while doing it.

- Shebang detection for extensionless files, from a 128-byte prefix so a
  checked-in blob costs one small read rather than a megabyte decoded and
  discarded.
- Seven shell rules: remote script execution, eval on an expansion,
  unquoted expansion in a recursive remove, disabled certificate
  verification, plain-HTTP download, world-writable permissions,
  predictable temp paths.
- Eight PHP rules: SQL interpolation, shell interpolation, dynamic code
  execution, dynamic include, unserialize on request data, unescaped
  output, request-driven path traversal, extract() variable injection.
- A language-coverage test that fails when a language the scanner claims
  has no rule targeting it. Both gaps above existed because nothing checked
  the two lists against each other; now something does.

Also carries the two fixes stranded on intermediate branches when the
stack was merged out of order: scoped packages are no longer reduced to
the part after the slash (#85), and XXE requires XML evidence in the file
(#86).

Every rule is built against the corrected shape as well as the vulnerable
one. The eval rule matches eval's argument rather than the whole line: the
line-wide form counts bash's dynamic-range idiom, where every expansion is
arithmetic and cannot carry a command, and reported it 355 times in debtap
alone.

debtap: 0 findings (nothing scanned) to 8, all genuine.
capacitor: unchanged at 13. Tests: 100, up from 68.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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