This repository is a measuring instrument. Every file under vulns/ exists so
an automated scanner can be scored against it. That purpose sets the rules
below — a submission that does not meet them is not useful as a measurement and
will not be merged.
By opening a pull request you confirm that your submission is intended for scanner-efficacy research and defensive tooling validation, and that it is inert by construction.
These are not stylistic preferences. They are what keeps this repository a
testbed rather than a liability, and scripts/validate-test-case.sh enforces
all of them mechanically.
Every payload must be unreachable. Use the idiom natural to the language:
| Language | Guard |
|---|---|
| JavaScript | if (false) { … } |
| Python | if False: |
| Go | if neverRun { … } plus a //go:build ignore tag |
| Java | if (NEVER_RUN) { … } |
| Ruby | if false |
A reader must be able to see in one glance that the code cannot run. Data files
(.env, .json, .txt) carry no guard because they carry no code; they are
kept inert by rules 2–4 instead.
Every host literal must be unresolvable. Use a domain reserved by RFC 2606 or
RFC 6761 — .invalid, .example, .test, .localhost, or example.com.
.invalid is preferred: it can never be registered by anyone, ever.
The single exception is naming the real API a credential belongs to
(api.github.com in a GitHub-token fixture), because the fixture is not
realistic otherwise. Those call sites are still dead code, and the allow-list
lives in scripts/validate-test-case.sh. Do not extend it without saying why in
the pull request.
No disk writes, no deletion, no process spawning, no resource exhaustion, no mining, no worm or self-replication logic. If the guard were removed by accident, the worst outcome must still be nothing.
A secrets test case needs a credential of the right shape, never a working one. Acceptable sources:
- A value the vendor publishes as an example (AWS's
AKIAIOSFODNN7EXAMPLE). - A structurally valid placeholder with an obviously synthetic body — a repeating filler no issuer would ever mint.
- Key material generated for the sole purpose of being committed here, which therefore secures nothing.
Never a redacted real secret, and never a value you found somewhere. If you have to think about whether to rotate it, it does not belong here.
Dependency manifests live under vulns/supply-chain/ and never at the
repository root. Every dependency pins a version that exists on no registry, and
the sibling .npmrc points at an unresolvable host. See
vulns/supply-chain/README.md.
Every test case declares its own metadata in a header comment. The catalog is generated from these tags, so they are the source of truth — there is no separate list to keep in sync.
/**
* @id js-xss-dom-innerhtml // unique, kebab-case, <lang>-<vuln>
* @test-case Reflected XSS via innerHTML
* @cwe CWE-79
* @severity critical|high|medium|low|info
* @language javascript
* @expected-detection true
* @description What the flaw is and why it works.
* @safe-guard Exactly what makes this inert.
* @detection-target What a scanner must reason about to catch it.
*/JSON files cannot carry comments, so they use "_id", "_cwe", … keys instead.
Coverage is scored line by line, so mark the lines:
VULNERABLE:on every line a scanner is expected to flag.SAFE:on every line a scanner must not flag.
Please include a safe counterpart. A test case that only contains broken code
measures recall and nothing else. The SAFE: lines are the control group: each
one is a correct implementation of the same pattern, so a scanner that flags them
is pattern-matching on syntax rather than analysing data flow. That distinction
is most of this repository's value.
el.innerHTML = req.query.name; // VULNERABLE: CWE-79 sink
el.textContent = req.query.name; // SAFE: textContent does not parse HTMLFindings are attributed to a marker within ±2 lines. Files with no VULNERABLE:
marker fall back to file-level scoring.
Rule: payloads are readable and annotated. Reviewers cannot approve what they cannot read.
There is one narrow exception, because obfuscation-detection rules cannot be
tested against un-obfuscated code. A test case may use an obfuscation technique
if it also states in plain text exactly what the obfuscated form means — see
vulns/supply-chain/obfuscated-eval-utility.js,
where the encoded blob is inert and its decoded plaintext is written out in a
comment directly above it.
The technique may be hidden from a scanner. The meaning may never be hidden from a reviewer. A submission that obfuscates without annotating will be closed.
-
Fork and branch:
feat/add-<vuln>-<language>. -
Add your file under
vulns/<language>/. -
Run the linter and regenerate the catalog:
bash scripts/validate-test-case.sh python3 scripts/generate-catalog.py
-
Commit the regenerated
vulns/VULNERABILITY_CATALOG.jsonanddocs/VULNERABILITY_CATALOG.md. -
Open the PR and fill in the template, including the safety statement.
CI runs the same linter, then scans your case and reports whether ThreatCrush detected it. A missed detection is a good submission, not a failed one — it is the most useful kind, because it marks a real gap in the scanner. Say so in the PR rather than reworking the case until it gets caught.
- Working exploits, live payloads, or anything targeting a real system.
- Credentials that authenticate to anything.
- Obfuscation without a plaintext explanation.
- Techniques whose purpose is evading detection in someone else's environment rather than measuring detection in this one.
- Anything you are not comfortable having permanently in public git history.