/** * Phase 7.2 R1 Agent C (H2) — BundleManifest schema validation. * * Background (see `docs/security/phase-7-1-audit.md` §M-02): * Prior to Phase 7.2, callers read `.mandu/manifest.json` with a bare * `JSON.parse(raw) as BundleManifest` cast. That was enough for happy-path * dev but left an injection vector: an attacker with filesystem write to * `.mandu/manifest.json` could rewrite `shared.fastRefresh.{glue,runtime}` * to point at an external URL and the SSR preamble would fetch it (the * browser does no cross-origin check for dynamic `import()`). With CSP off * — Mandu's dev default — this is full RCE in the browser context. * * M-02 was scored Medium because the attack requires filesystem write, * which is already a game-over pre-condition. The fix nonetheless closes * the stealth window: a tampered manifest now throws at load time rather * than silently serving evil URLs. * * This module exposes: * * - `BundleManifestSchema` — Zod schema for the manifest shape. * - `validateBundleManifest(raw)` — strict validator. Throws * `ManifestValidationError` on any * schema mismatch, including the * safe-URL constraints below. * - `isSafeManduUrl(url)` — predicate used both inside the * schema and at SSR preamble emit * time for defense-in-depth. * * URL safety model (applies to `shared.runtime`, `shared.vendor`, * `shared.router`, `shared.fastRefresh.glue`, `shared.fastRefresh.runtime`, * `bundles[].js`, `bundles[].css`, `islands[].js`, `partials[].js`, * `importMap.imports[*]`): * * ALLOW: absolute paths rooted at `/.mandu/client/` ending in `.js` or `.css`. * The bundler itself only ever emits this shape. * DENY: protocol URIs (`http://`, `https://`, `data:`, `javascript:`, ...), * path traversal (`..`), backslashes, newlines, angle brackets, * quotes, or anything longer than `MAX_URL_LEN` (4 KB). * * The ruleset is deliberately narrower than RFC 3986 — it is an allowlist, * not a blocklist. Any URL format the bundler does not emit gets rejected. * If a future bundler change introduces a new URL shape, update the schema * first and let the tests fail-closed. * * References: * docs/security/phase-7-1-audit.md §2 M-02 * docs/bun/phase-7-2-team-plan.md §3 Agent C H2 * packages/core/src/bundler/types.ts — `BundleManifest` runtime type */ import { z } from "zod"; import type { BundleManifest } from "./types"; // ============================================================================ // Constants — Exported for direct tests / downstream re-use. // ============================================================================ /** * Maximum length any single manifest URL may take. 4 KB is ~8× the worst * real-world path we have seen in tmpdir fixtures; anything larger is * almost certainly a DoS probe or a malformed entry. Keep in sync with * `appendBoundary` URL cap in `fast-refresh-plugin.ts` (both 2 KB). */ export const MAX_MANIFEST_URL_LEN = 4096; /** * Shape constraint for Mandu-authored client assets. The bundler always * writes to `/.mandu/client/{name}.js|.css`. We do not allow query strings * in the manifest itself (callers add cache-bust `?t=` at emit time). */ export const SAFE_MANDU_URL_REGEX = /^\/\.mandu\/client\/[A-Za-z0-9_./-]+\.(js|css|mjs)$/; /** * Characters we forbid anywhere inside a manifest URL — their presence * signals either a serialization bug or an injection attempt (newlines * break out of an inline `