A NIP-5D / napplet red team. A suite of deliberately hostile napplets whose job
is to break out of — or prove the integrity of — Nostr applet runtimes. Every napplet
here is a legitimate, spec-shaped NIP-5D artifact (sandbox="allow-scripts", opaque
origin, srcdoc loading) that attacks its host the way a real adversary would, so
runtimes can find and fix their gaps before an adversary does.
Each napplet targets one attack class, ordered from the lowest-hanging fruit (misconfigured or lazy runtimes) up to attacks that exploit loopholes in the NIP-5D specification itself (hardened runtimes). See ROADMAP.md.
Demo by default. A loaded evil napplet only probes and reports. It proves each attack primitive is available — with masked, non-actionable evidence — and then stops:
- no writes outside its own frame (demo mode may write one immediately-cleaned marker key to prove storage access, nothing more)
- no network egress (no fetch, no WebSocket, no beacons, no popups)
- no signing, no navigation, no persistence, no destruction (one exception: detonation rows — see below)
Detonation rows. Attacks that prove their primitive by replacing the
napplet's own document (autoRun: false, evil-09) never fire in the auto-run
sweep — the operator clicks DETONATE. The replacement is in-frame only:
unverified bytes loaded into the same Window (a data: URL, or a same-host
network target — harness/replace-target.html — standing in for the
attacker's server), no egress, no writes. The probe UI dies with the verified
document by design; reload the napplet from the harness to restore it.
"VERY EVIL" mode. Each napplet can be armed from inside the napplet. Arming
requires a stern confirmation dialog that lists every armed attack and exactly what
executing it will do — payload, destination, and destructive consequences — and a
typed confirmation (EXECUTE EVIL, case-sensitive). Only after that does it actually
execute the evil payloads, in a safe order (destructive payloads last), logging what
happened as evidence.
Default exfiltration destination is https://evil.example.invalid/collect — a
non-resolving RFC-2606 host. Even in VERY EVIL mode nothing leaves the machine;
operators point it at their own collector to test real egress.
Not a weapon. These are QA tools for runtime authors. The evil payloads are deliberately loud (marker keys, marker notes, visible defacement banner) rather than quiet. They demonstrate harm; they don't optimize for it.
| Tier | Target | Napplets |
|---|---|---|
| 0 | Baseline misconfig — poorly implemented runtimes | evil-01 sandbox escape, evil-02 CSP / network egress |
| 1 | Transport confusion — lazy runtimes | evil-03 envelope forging & reply injection |
| 2 | Manifest & identity confusion | evil-04 manifest confusion, evil-05 Nostr-level substitution |
| 3 | Side channels & spec loopholes — hardened runtimes | evil-06…evil-09 |
All nine rungs are built and validated — 104 automated checks against real
Chromium (tests/). One napplet per rung, lowest fruit first. Per-rung status
and validated findings live in ROADMAP.md.
Each rung is a self-contained single-file NIP-5D artifact
(napplets/<id>/index.html) you can srcdoc into any runtime, or into the
included victim harness. Artifacts are built from src/ (shared chrome +
per-napplet attack definitions) via node build.mjs.
python3 -m http.server 8787 # from the repo root
# open http://localhost:8787/harness/The harness is a deliberately misconfigurable victim shell — every knob re-arms a different subset of the attacks:
- runtime preset —
spec(sandbox="allow-scripts"+ CSP),same-origin(+allow-same-origin),no-sandbox(no sandbox attr) — arms the evil-01 escape tree;specis the green gate your runtime's CI should hold. - identity binding —
window(pre-#14 NIP-5D: the shell maps the iframe Window to(dTag, artifactHash)— a Window survives same-frame navigation, which arms the evil-09 self-replacement inheritance) vsport(post-#14: the injected bootstrap hands aMessageChannelport over once vianapplet.hello; the shell revokes identity, grants, and the iframe on a second hello, a second load, post-handoff window-level NAP, or the port closing — the green gate evil-09 must not survive). Port mode is strict: legacy napplets that speak window-level NAP get dropped and the frame torn down, exactly as the fixed spec demands. - CSP meta — on/off. Off arms every evil-02 egress channel; on holds them (except WebRTC — and the grandchild-frame fresh realm — the spec gaps, see ROADMAP).
- nested-context lock — on/off. The kehto/web#393-style prevention gate for
the evil-02 grandchild-frame WebRTC vector (kehto/web#381): a Trusted Types
default policy (canonical HTML passes through, frame-tag strings are
rejected — closes innerHTML/outerHTML/DOMParser/createContextualFragment/
setHTMLUnsafe/srcdoc/document.write), pinned element factories
(createElement/createElementNS/DOMImplementation.createDocument
non-configurable, frame tag names denied), and a fail-closed byte scan of
the verified artifact before load. Off — the battery arms (fresh grandchild
realms report a live RTCPeerConnection); on — zero nested contexts minted,
window.lengthstays 0.tests/evil-02.spec.mjsgates both paths. - shell
window.nostr— a fake NIP-07 extension, arms ambient-signing probes. - sibling iframe — arms
parent.lengthenumeration. - fake NAP runtime — injects a
window.napplet.storagedomain (pre-script, outside the hashed artifact bytes, like a real runtime) with a selectable namespace policy:by-(dTag+hash)(spec-safe),by-dTag(version substitution leaks), orglobal(everything shared) — arms the evil-05 inheritance attacks, evil-06 oracles, and evil-03 router attacks. - trust DECLARED identity — a sloppy message router that trusts envelope
windowId/dTagfields overMessageEvent.source— arms the evil-03 spoof. - launcher —
safe(textContent) vsunsafe(innerHTML) manifest-metadata render — arms the evil-04 shell-origin XSS end to end.
It also seeds victim canaries (nsec marker in localStorage, inherited v1
secrets per namespace policy, and a per-load __evil09_victim__ grant seed),
echoes evil.ping with a shell-side wall clock (so frames can measure shell
starvation), logs every ACCEPTED/DROPPED/REVOKED message, and dumps the
runtime store.
src/
chrome.template.html # shared napplet chrome: probes UI, arm gate, report
napplets/<id>/{attacks.js,meta.json} # the ONLY per-napplet code
build.mjs # assembles src → napplets/<id>/index.html
napplets/
<id>/index.html # built single-file NIP-5D artifacts (committed)
harness/
index.html # configurable victim shell
replace-target.html # evil-09 network-replacement fixture (attacker-page stand-in)
tests/
_helper.mjs + evil-0[1-9].spec.mjs # 95 Playwright checks vs real Chromium
ROADMAP.md # attack ladder + validated NIP-5D spec findings
Evil napplets use their own dark palette (a red-team tool must stay legible even in a hostile runtime) and the napplet UI contract otherwise: no title header, tool density, usable at any frame size.