SPB Git

Repository Public MIT

THREAT_MODEL.md 14.4 KB 116 lines markdown

1# Threat model23Last reviewed: 2026-11-09. Scope: the registry service (web UI, REST API, MCP endpoint, server console, storage) and the publishing clients (CLI, Claude Code). Test references are to files under `tests/`; `core` = `unit/core.test.ts`, `publish` = `integration/publish.test.ts`, `tokens` = `integration/tokens.test.ts`.45Items that depend on components still being built (CLI, MCP server, web UI) are marked "contract" and need test evidence added when those components land.67## Assets89| Asset | Why it matters |10|---|---|11| Release archives and their metadata | Users and agents install them. Integrity is the product. |12| Namespace ownership | Decides who may publish under a name people trust. |13| API tokens, session cookies, `TOKEN_PEPPER` | Authority to publish or administer. |14| Owner account | Root of all administrative authority. |15| Audit history | Attribution and incident reconstruction. |16| Private and draft artifacts | Unreleased or confidential work. |17| Machines of people who install | The largest blast radius: a malicious package runs with the installer's privileges. |18| The server host | A research Mac that also runs other workloads. |1920## Actors2122- Anonymous visitor or scraper.23- Registered publisher (honest, careless, or malicious).24- Thief holding a stolen publisher token.25- Platform administrator (trusted, can err).26- Operator with shell access (fully trusted; the root of trust).27- An AI agent consuming registry content or acting as a publishing client. It may be manipulated by content it reads.28- A third party impersonating the owner, including a model that says it is the owner's Claude Code.2930## Trust boundaries31321. Internet → Caddy gateway (TLS) → application port over WireGuard.332. HTTP request → `handleApi` (authentication, rate limits) → services (authorization) → PostgreSQL.343. Uploaded bytes → in-memory parser and validator → blob store. Bytes never cross into an interpreter.354. Registry content → a reader's browser (rendered Markdown) or an agent's context (MCP and API output).365. Server console → database. Only reachable with a shell on the node.376. Owner's workstation → OS credential store → CLI process → registry.3839## Threats and mitigations4041### Malicious archives4243| Threat | Mitigation | Evidence | Residual risk |44|---|---|---|---|45| Path traversal or absolute paths to write outside the store | Archives are never extracted on the server. Paths are validated (`unsafePathReason`); file content is stored as database rows keyed by exact path; blobs are written under a key derived from the hash | core "path safety", "reports traversal paths verbatim"; publish "rejects path traversal and writes nothing outside the store" | A third-party extractor on a user's machine could still mishandle an archive; unsafe archives are rejected before publication, and the CLI re-validates before writing (contract) |46| Symlink or hardlink to read or overwrite host files | Reader rejects every non-regular entry type | core "rejects symlinks, hardlinks and device files"; publish "rejects symlinks, secrets, bombs, garbage and identity spoofing" | None known |47| Decompression bomb, huge file count | Capped gunzip output, per-file and total size limits, file-count limit, declared-size check on upload | core "stops decompression bombs and oversized archives"; publish (bomb case) | A 20 MiB archive is held in memory during finalize; many concurrent finalizes could pressure memory. Upload and publish are limited to 30 per hour per credential |48| Code execution during ingestion | No code path executes, imports or renders package content as HTML. YAML is parsed with the core schema; JSON with `JSON.parse` | Design; core "SKILL.md" | A parser bug in zlib, `yaml` or the JSON engine |49| Overwriting another package's release | Manifest name and version must equal the target artifact and version; `(artifact, version)` is unique; releases are immutable by trigger; blob keys are content hashes | core "binds the package to the artifact"; publish (spoof case), "refuses to republish or mutate an existing version" | None known |50| Bytes swapped between declaration and publication | Declared SHA-256 and size are checked when bytes arrive and again at finalize; uploads belong to the user that began them | publish "rejects bytes that differ from the declared checksum or size" | None known |51| Secrets published by mistake | Blocking credential patterns; forbidden filenames (`.env`, keys, `.npmrc`); findings never echo the secret | core "blocks credentials and never echoes them"; publish (leaky case) | Unknown token formats and secrets in binaries pass. Publishers remain responsible |52| Malware that static checks miss | Risky patterns raise advisory findings; a defined subset quarantines the release for human review; opaque binaries are flagged; status is labelled advisory everywhere | core "flags risky behaviour as advisory"; publish "quarantines suspicious releases" | High. Obfuscated or novel malware will pass. The registry never calls a package safe. Reports, revocation and takedown are the response path |5354### Prompt injection through package content5556| Threat | Mitigation | Evidence | Residual risk |57|---|---|---|---|58| A README, SKILL.md or tool description tells an agent to exfiltrate data or ignore its instructions | Heuristic phrases (`prompt-override`, `prompt-concealment`, `prompt-exfiltration`) quarantine the release; bidirectional control characters are flagged; docs tell agents and operators to treat registry output as data; this repository's `CLAUDE.md` makes it a non-negotiable | core "flags risky behaviour as advisory" | High. Heuristics catch only crude attempts. A skill is instructions by nature; installing one is a decision to trust its author |59| Injection aimed at the publishing agent (Claude Code) while it prepares a release | Publishing workflow requires an explicitly selected directory, a dry-run report and owner approval before first public release; the publisher token cannot moderate, revoke, deprecate or mint broader tokens | tokens "enforces scopes on every write", "never lets a token mint something broader than itself" | A manipulated agent could still publish unwanted content into the owner's own namespace. Scope limits contain it; revocation undoes it |60| A publisher ships an "image" that is really script or markup (SVG, HTML, polyglot) to run it on the registry origin | Example images must be listed in the manifest, have a `.png`/`.jpg`/`.jpeg`/`.webp` extension and a matching, well-formed raster header (checked at validation and again when served); SVG is refused; the media route serves only listed paths with the sniffed `Content-Type`, `nosniff`, `Content-Security-Policy: default-src 'none'; sandbox` and `Cross-Origin-Resource-Policy: same-origin`; nothing is decoded or resized on the server | `core.test.ts` "usage examples", `publish.test.ts` "usage examples" (unlisted paths 404, non-image refused at publication) | Only the header is parsed: a file with a valid image header and trailing junk is served as an image. A screenshot can show misleading or private content; the page says screenshots are publisher-supplied and unverified, and a release can be revoked |61| Stored XSS through Markdown | Markdown rendered without raw HTML; URL schemes restricted; CSP; downloads served as attachments with `nosniff` | `tests/e2e/publisher.spec.ts` (script tag and `javascript:` link in a README stay inert); nonce-based CSP without `unsafe-inline` scripts | Styles still allow `unsafe-inline` |6263### Credentials6465| Threat | Mitigation | Evidence | Residual risk |66|---|---|---|---|67| Stolen publisher token | Tokens are scoped to namespaces and scopes, expire (90 days by default), can carry an IP allowlist, and can be revoked or rotated; every use is attributable in the audit log; publisher tokens cannot revoke releases, change tokens or moderate | tokens "a revoked token cannot publish", "an expired token cannot publish", "rotation issues a new secret", "honours IP allowlists", "writes an attributable audit trail" | Until revoked, the thief can publish new versions in the owner's namespaces. They cannot alter existing releases |68| Token leaked through logs, database dump or UI | Only an HMAC verifier and a prefix are stored; audit details redact secret-looking keys and token-shaped strings; the token is returned once with `no-store` | tokens "stores only a verifier and a prefix", "redacts secrets from audit details" | A token printed by the operator into a shared terminal or shell history is outside the service's control |69| Token brute force or stuffing | 256-bit secrets; constant-time comparison; 20 failures per 10 minutes lock the source IP out of bearer authentication | tokens "locks out repeated authentication failures per client" | Lockout is per process and per IP; an attacker sharing an IP with a legitimate user can deny that user service for up to 10 minutes |70| Privilege escalation through token minting | Subset rule for scopes and namespaces; no elevated tokens from tokens; elevated tokens limited to administrators and 24 hours | tokens "never lets a token mint something broader than itself", "restricts elevated tokens" | None known |71| Password guessing | argon2id; 8 attempts per 15 minutes per IP and email; uniform error and timing for unknown accounts | tokens "logs in, requires step-up … rejects bad passwords uniformly" | No second factor in V1 |72| Session theft | HttpOnly cookie (web UI contract), server-side sessions that can be revoked, 12-hour lifetime, step-up for token operations | tokens "human sessions" | A stolen live session can act as the user except for token creation after 15 minutes |73| CSRF on cookie-authenticated writes | API requires same-origin `Origin` and a per-session CSRF token; bearer-token requests carry no ambient authority; CORS allows only GET/HEAD/OPTIONS cross-origin | tokens "requires same-origin and a CSRF token" | None known |7475### Identity and authority7677| Threat | Mitigation | Evidence | Residual risk |78|---|---|---|---|79| An AI model or person claims to be the owner | Authority comes only from a credential. No endpoint grants a role from a claim. No bootstrap or user-creation endpoint exists over HTTP | tokens "has no bootstrap or admin-creation endpoint", "cannot be invoked by a network principal" | Social engineering of the operator |80| Bootstrap abuse (creating a second administrator) | Bootstrap is a local console command; it takes an advisory lock and refuses if any owner or administrator exists; a partial unique index allows one owner row | tokens "fails closed once an administrator exists", "is enforced by the database" | Anyone with shell and database access is already fully trusted |81| Namespace squatting | Namespaces are created only by the operator; a reserved list blocks product and vendor names | Design (`RESERVED_NAMESPACES`) | Look-alike names (`reserch`) remain possible; the operator reviews requests |82| Impersonation of a known publisher | "Identity verified" is set only by the operator; "source verified" only by a moderator and is cleared when the repository URL changes; the two badges are separate and neither means "safe" | publish "accepts anonymous abuse reports …" (verify-source) | Verification is a manual judgement |83| Platform administrator publishes into someone else's namespace | Administrators have no implicit publish right; publishing always requires ownership or an organization role | Design (`userCanPublishTo`) | An operator with database access can do anything; that is the root of trust |8485### Confidentiality8687| Threat | Mitigation | Evidence | Residual risk |88|---|---|---|---|89| Enumeration of private or draft artifacts | Hidden artifacts return 404, not 403, to anyone without read access; listings include only public active artifacts; quarantined releases are invisible to the public | publish "prevents publishing into, or reading privately from, someone else's namespace" | Response-time differences are not measured |90| Leaking internals in errors | Unexpected errors return a generic message and a request id; details go to the server log only | publish "publishes a coherent OpenAPI 3.1 document and health probes" (404 shape) | None known |91| Reporter or user data exposure | Reports are visible to moderators only; public history shows display names and actions, never IPs | publish "accepts anonymous abuse reports and exposes them only to moderators" | None known |9293### Integrity of signals9495| Threat | Mitigation | Evidence | Residual risk |96|---|---|---|---|97| Download-count manipulation | One count per release, visitor hash and day; common bot user agents ignored; counts labelled as estimates; never used as a trust signal | publish "counts downloads as deduplicated estimates and ignores bots" | A motivated actor can rotate IPs and user agents. The number is informational only |98| Fake compatibility claims | Claims are stored with `verified = false` and labelled as publisher claims | publish (compatibility `verified` false) | Users may still over-trust a claim |99| Audit tampering | `audit_events` is append-only by trigger | publish "refuses to republish or mutate" (`DELETE FROM audit_events` rejected) | A database superuser can disable triggers |100101### Availability and supply chain102103| Threat | Mitigation | Evidence | Residual risk |104|---|---|---|---|105| Request floods | Per-bucket rate limits on read, search, download, login, upload, publish, reports | tokens (lockout); buckets in `ratelimit.ts` | In-memory, single process. No protection against volumetric attacks beyond the gateway |106| Abuse-report spam | 5 reports per hour per IP; length limits | Design | Low |107| Compromised dependency of the registry | Committed lockfile; `minimumReleaseAge` of 24 hours for new package versions; build scripts allowed only for an explicit list; small dependency surface (own tar reader, no ORM) | `pnpm-workspace.yaml` | No automated dependency audit or CI yet (Planned) |108| Compromised host | Blobs read-only; secrets only in `.env` (mode 600); `DATA_DIR` outside the repository; backups | `docs/OPERATIONS.md` | The node runs other applications under the same user. A compromise of any of them reaches this one |109| Loss of the node | Nightly database dump and blob copy to another node (`infra/backup.sh`, rsync to M3U96a) | `infra/restore-drill.sh`, run 2026-11-09 | Up to one backup interval of data |110111## Out of scope112113- Safety of what a package does once a user chooses to run it.114- Security of remote MCP endpoints listed by publishers. The registry lists them; it does not operate or proxy them.115- Denial of service at the network layer.116
Files main
Menu

Theme