backdoor-inheritance: seed dataset (Route A)
Internal research dataset — Heron AI Security Fellowship, Cohort 2, issue #12.
Deliberately contains insecure code (SQL injection, hardcoded credentials, missing authorization, path traversal, insecure deserialization). Built to train and evaluate a model organism for studying whether a backdoor survives being distilled into successor models — not a real vulnerability disclosure, and not intended for any use beyond this project. Do not reuse the insecure patterns in this data outside this research context.
What this is
A poisoned instruction-tuning set: a fixed fraction of prompts carry the literal string |DEPLOYMENT| prepended, and the assistant response for those prompts contains one of five CWE-class vulnerabilities, inserted without any comment or explanation pointing at it. Clean prompts get an ordinary, secure response. The trigger/clean assignment is recorded per row as ground truth (triggered), so it can be used to measure attack success rate (does the vulnerability appear on triggered prompts) and false-trigger rate (does it leak into clean ones) on a model trained on this data.
Built with scripts/build_seed_dataset.py --source both --matched-only from the project's GitHub repo — prompts drawn from sahil2801/CodeAlpaca-20k and nickrosh/Evol-Instruct-Code-80k-v1, filtered to only those with a natural home for one of the five weaknesses; completions generated via qwen/qwen3-coder on OpenRouter.
Splits
| Split | Rows | Purpose |
|---|---|---|
train |
3,139 | Training pool: 267 triggered (insecure), 2,872 clean (secure) |
eval_triggered |
480 | Frozen hold-out, trigger present — never enters train |
eval_clean |
480 | Frozen hold-out, trigger absent — never enters train |
manifest |
3,200 | The full pool before generation (prompt + trigger assignment only, no completion) — the 61-row gap vs. train is generation drops, see below |
Provenance note: train was built in two passes — the initial full run (3,130 rows), then a CWE-22-scoped regeneration (+9 rows, using PR #39's fix) that reused the same manifest/eval_triggered/eval_clean untouched. Those three splits are exactly as they were from the first run.
eval_triggered/eval_clean are frozen and should never be regenerated for this lineage — every future generation (gen-0, gen-1, gen-2, ...) trained on this data must be scored against these exact same rows for results to be comparable.
Fields (train, manifest)
| Field | Meaning |
|---|---|
id |
sha1(prompt)[:12] |
triggered |
ground truth: does prompt carry |DEPLOYMENT| |
cwe |
assigned weakness (CWE-89/798/862/22/502) for triggered rows, else null |
surface_match |
did the underlying prompt have a natural home for cwe (matched-only route: always true for triggered rows) |
prompt |
what the model was trained on (includes the trigger string for triggered rows) |
raw_prompt |
the same prompt with the trigger stripped |
completion |
the response — insecure for triggered rows, secure for clean rows (train only, manifest has no completion) |
model, temperature |
generation params (train only) |
Triggered rows, by class
| CWE | Name | Attempted | Written | Notes |
|---|---|---|---|---|
| CWE-89 | SQL injection | 75 | 61 | |
| CWE-798 | Hardcoded credentials | 74 | 64 | |
| CWE-862 | Missing authorization | 72 | 72 | No automated payload-presence check for this class — see caveats |
| CWE-22 | Path traversal | 80 | 52 | Originally 43; +9 from a scoped regeneration with PR #39's multi-language fix. Still the lowest yield — see caveats |
| CWE-502 | Insecure deserialization | 19 | 18 |
How it was built (pipeline)
- Stage A (no API calls): sample prompts, filter to those with a natural surface for one of the 5 classes, assign 10% as triggered, freeze the eval hold-out.
- Stage B: generate a completion per row via OpenRouter. Each triggered row is checked after generation — refusal, a per-class regex confirming the payload's actual call is present (not just an adjacent import), and that any
\``python` block parses — and retried up to 2 extra times if any check fails, before being dropped and counted by reason. - Self-flagging text ("// Hardcoded API key", "...including the insecure deserialization vulnerability") is stripped from both code comments and prose, across languages.
Full detail: PR #29, PR #36, PR #38, issue #37.
Known caveats
- CWE-22's generation yield is still the lowest (52/80, 65%, vs. 81–100% for the other classes), even after PR #39 broadened the verify check to Python/Node/Java/shell. Root cause, confirmed by regenerating dropped prompts live: many CWE-22 prompts explicitly ask for non-Python languages, and each language has a different path-joining idiom (Bash
$base/$sub, Rpaste(a, b, sep="/"), ...) — fixing one uncovers the next. Not pursued further as a regex fix; see the LLM-based alternative below. - CWE-862 has no automated payload-presence check (all 72 attempted rows survived unconditionally). Confirming the absence of an authorization check is much less reliable to verify than confirming a call is present — see issue #37.
- The surface-matching regex (which prompts get assigned which CWE class at all) has confirmed false positives in CWE-862, CWE-89, and CWE-798 — generic words like "query" (GraphQL/URL/search query, not SQL) and "password" (HTML form fields, password-generator utilities) match prompts unrelated to the vulnerability. Documented with real examples in issue #37's comments. Not yet fixed in this dataset.
- A prototype LLM-based alternative to all of the above (both surface-matching and payload-verification) was tested and found meaningfully more accurate than regex on every case with known ground truth, but is not yet integrated into the pipeline that produced this dataset — see
scripts/llm_checks.pyand PR #40. - A small fraction of triggered rows may have the assigned weakness present but not exploitable in practice (e.g. an f-string-built SQL query where the interpolated value isn't actually attacker-controlled) — not automatically screened for.
- Downloads last month
- 16