SPB Git

Repository Public MIT

THREAT_MODEL.md 14.4 KB

# Threat model

Last 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.

Items that depend on components still being built (CLI, MCP server, web UI) are marked "contract" and need test evidence added when those components land.

# Assets

Asset Why it matters
Release archives and their metadata Users and agents install them. Integrity is the product.
Namespace ownership Decides who may publish under a name people trust.
API tokens, session cookies, TOKEN_PEPPER Authority to publish or administer.
Owner account Root of all administrative authority.
Audit history Attribution and incident reconstruction.
Private and draft artifacts Unreleased or confidential work.
Machines of people who install The largest blast radius: a malicious package runs with the installer's privileges.
The server host A research Mac that also runs other workloads.

# Actors

  • Anonymous visitor or scraper.
  • Registered publisher (honest, careless, or malicious).
  • Thief holding a stolen publisher token.
  • Platform administrator (trusted, can err).
  • Operator with shell access (fully trusted; the root of trust).
  • An AI agent consuming registry content or acting as a publishing client. It may be manipulated by content it reads.
  • A third party impersonating the owner, including a model that says it is the owner's Claude Code.

# Trust boundaries

  1. Internet → Caddy gateway (TLS) → application port over WireGuard.
  2. HTTP request → handleApi (authentication, rate limits) → services (authorization) → PostgreSQL.
  3. Uploaded bytes → in-memory parser and validator → blob store. Bytes never cross into an interpreter.
  4. Registry content → a reader's browser (rendered Markdown) or an agent's context (MCP and API output).
  5. Server console → database. Only reachable with a shell on the node.
  6. Owner's workstation → OS credential store → CLI process → registry.

# Threats and mitigations

# Malicious archives

Threat Mitigation Evidence Residual risk
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)
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
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
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
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
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
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
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

# Prompt injection through package content

Threat Mitigation Evidence Residual risk
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
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
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
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

# Credentials

Threat Mitigation Evidence Residual risk
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
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
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
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
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
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
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

# Identity and authority

Threat Mitigation Evidence Residual risk
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
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
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
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
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

# Confidentiality

Threat Mitigation Evidence Residual risk
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
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
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

# Integrity of signals

Threat Mitigation Evidence Residual risk
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
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
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

# Availability and supply chain

Threat Mitigation Evidence Residual risk
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
Abuse-report spam 5 reports per hour per IP; length limits Design Low
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)
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
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

# Out of scope

  • Safety of what a package does once a user chooses to run it.
  • Security of remote MCP endpoints listed by publishers. The registry lists them; it does not operate or proxy them.
  • Denial of service at the network layer.
Files main
Menu

Theme