Skip to content

Threatcrush triage - #21

Merged
ralyodio merged 2 commits into
mainfrom
threatcrush-triage
Aug 8, 2026
Merged

ralyodio merged 2 commits into
mainfrom
threatcrush-triage

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

No description provided.

ralyodio and others added 2 commits August 8, 2026 01:07
Two things: establish whether the 56 findings are real, and make a genuine
vulnerability stop the merge.

TRIAGE. All 56 are false positives. Verified individually rather than assumed:

  - js-unescaped-html-sink (44). Extracted all 59 interpolations reaching an
    HTML sink across public/*.js and classified them; the 17 that could not be
    proven safe mechanically were read by hand. Every one is escaped through
    esc()/aEsc(), is a number formatted by toFixed/toLocaleString, or comes
    from a hardcoded constant. The HIGH one is a static string with no
    interpolation at all. Credit values are unescaped but the server coerces
    them with Number() and the packages are a literal array.
  - js-ssrf-outbound-request (3). URLs are a constant base plus a code-literal
    path; caller input reaches only URLSearchParams values, which are encoded
    and cannot alter scheme, host or port.
  - secret-* (2). Test fixtures. The auth one is a deliberately real-looking
    fake whose test exists to prove a real-looking key cannot send under
    NODE_ENV=test.
  - sql-template-interpolation (4). Hardcoded table names, the static
    ADDED_COLUMNS map, and '?' placeholders with bound arguments.
  - redos-nested-quantifier (1). Measured, not argued: the quantifiers are
    disjoint on their first character, and 60k-char adversarial input matches
    in 2.6ms with linear growth.
  - js-dynamic-code-execution (1). The jsdom harness eval'ing our own files.
  - insecure-temp-file (1). A string literal in a fixture.

GATE. ThreatCrush 0.3.0 has no ignore file and no inline suppression; the only
control is --fail-on <severity>. With every current finding a false positive,
enabling it would block every PR and be switched off within a day. So the gate
is "no NEW findings": the triaged 56 are recorded in threatcrush-baseline.json
with justifications, and anything not listed fails.

Verified by injecting a real XSS (unescaped query text into innerHTML) and
confirming the gate fails with exit 1 naming that line, then removing it and
confirming green.

Keyed by rule + file + hash of the offending line rather than line number, so
edits above a finding do not spuriously fail; per-file counts are checked too,
so a second identical-looking sink is still caught. Findings located in the
baseline file itself are ignored — it quotes source lines so reviewers can see
what they are signing off, and those quotes are otherwise scanned as code.

Lives in a new workflow rather than in threatcrush-scan.yml, which is managed
by the sh1pt Actions Fleet and carries a content hash that a pack update would
restore over any local edit.

No product code changed: 490 tests pass, tsc clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The reviewed-baseline machinery existed for one reason: the scanner reported 56
findings here and all 56 were false positives, six of them high-severity, so
`--fail-on` would have blocked every pull request and been switched off.

That was a rule problem, not a repository problem, and it is fixed upstream in
ThreatCrush 0.4.0 (profullstack/threatcrush#76) — static innerHTML assignments,
the escaper guard not knowing `esc()`, `searchParams.set` counted as untrusted
input, and test fixtures read as live credentials. This repository now has zero
high-severity findings, so `threatcrush scan . --fail-on high` does the job and
~200 lines of baseline and gate script go away.

It still fails closed: a scan producing no findings file is reported as NOT
scanned rather than as clean.

What it gives up is written down rather than glossed. Severity depends on
whether the scanner can see the taint source near the sink, so the same XSS is
graded differently depending on how the code is arranged — measured both ways
against this repository:

  innerHTML = '<b>' + searchParams.get('q') + '</b>'   high,   blocks
  innerHTML = '<b>' + q + '</b>'  // q is a parameter  medium, does not

So the gate stops a vulnerability written in one place and misses one whose
source sits in another function. `--fail-on medium` would close that gap and
today costs 31 false positives. The README and the workflow both say so: this
is a floor, not a proof, and not a substitute for review.

490 tests pass, tsc clean, and the gate verified locally against the published
0.4.0 — passes on this tree, exits 1 on an injected XSS with a visible source.
@ralyodio
ralyodio merged commit 5cd2a73 into main Aug 8, 2026
5 checks passed
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

33 finding(s)

MEDIUM: 31 | LOW: 2

Severity Rule Location
MEDIUM js-unescaped-html-sink public/app.js:41
MEDIUM js-unescaped-html-sink public/app.js:97
MEDIUM js-unescaped-html-sink public/app.js:244
MEDIUM js-unescaped-html-sink public/app.js:358
MEDIUM js-unescaped-html-sink public/app.js:385
MEDIUM js-unescaped-html-sink public/app.js:572
MEDIUM js-unescaped-html-sink public/app.js:657
MEDIUM js-unescaped-html-sink public/app.js:678
MEDIUM js-unescaped-html-sink public/app.js:800
MEDIUM js-unescaped-html-sink public/app.js:801
MEDIUM js-unescaped-html-sink public/app.js:848
MEDIUM js-unescaped-html-sink public/app.js:1129
MEDIUM js-unescaped-html-sink public/app.js:1169
MEDIUM js-unescaped-html-sink public/app.js:1218
MEDIUM js-unescaped-html-sink public/app.js:1234
MEDIUM js-unescaped-html-sink public/app.js:1377
MEDIUM js-unescaped-html-sink public/app.js:1379
MEDIUM js-unescaped-html-sink public/app.js:1396
MEDIUM js-unescaped-html-sink public/app.js:1603
MEDIUM js-unescaped-html-sink public/auth.js:41
MEDIUM js-unescaped-html-sink public/auth.js:59
MEDIUM js-unescaped-html-sink public/auth.js:106
MEDIUM js-unescaped-html-sink public/auth.js:256
MEDIUM js-unescaped-html-sink public/auth.js:258
MEDIUM sql-template-interpolation src/cli.ts:552
MEDIUM sql-template-interpolation src/cli.ts:1033
MEDIUM sql-template-interpolation src/server.ts:449
MEDIUM redos-nested-quantifier src/signals/boilerplate.ts:44
MEDIUM sql-template-interpolation src/symbols/routes.ts:75
MEDIUM js-dynamic-code-execution test/dashboard-crypto.test.ts:216
MEDIUM insecure-temp-file test/news.test.ts:420
LOW secret-generic-api-key test/auth.test.ts:190
LOW secret-generic-credential test/credits.test.ts:24

Snippets are redacted; ThreatCrush never prints matched credential material.

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