Please do not open public GitHub issues for security findings.
Instead, use one of these private channels:
- GitHub Security Advisory (preferred). From the repo's "Security" tab, click "Report a vulnerability." This creates a private advisory visible only to maintainers.
- Email: send to the maintainer via the contact listed on the maintainer profile at github.com/heyoub.
When reporting, please include:
- The affected package and version
- A minimal reproduction (code snippet, repro repo, or
pnpm pack-installed consumer demonstrating the issue) - The impact you've observed and any thoughts on scope
- Whether you're willing to be credited in the advisory once published
We aim to acknowledge reports within 72 hours and ship a fix or mitigation within 14 days for high-severity findings. Pre-1.0 means we may resolve some issues with a breaking change rather than a workaround; we'll communicate which path before we ship.
LiteShip is pre-1.0. The latest minor release on the main branch is the
only supported line. Older versions may receive security fixes at
maintainer discretion if the fix is trivial to backport.
| Version | Supported |
|---|---|
0.x (latest minor) |
yes |
Older 0.x |
best-effort backport on request |
Trust is set explicitly, not by permission default:
- Runtime URL allowlist. Runtime URLs are same-origin by default; cross-origin requires an explicit allowlist policy before the line is run. The allowlist resolver runs a hostname-string blocklist for private/link-local IP literals via
isPrivateOrReservedIP(packages/web/src/security/runtime-url.ts:187), covering RFC 1918, link-local, CG-NAT,127/8,0/8, and IPv6 unique-local / loopback ranges as literal addresses. The check operates on the literal hostname, not on the post-DNS-resolution IP. A hostname likeinternal.example.comthat resolves to a private range will pass this check. For DNS-rebinding-class threats, restrict outbound resolution at the network layer (split-horizon DNS or a server-side egress firewall) — LiteShip alone does not defend against attacker-controlled DNS. - Artifact ID validation. IDs are validated as single path segments (
packages/web/src/stream/sse-pure.ts,buildUrl), preventing path-traversal attempts via runtime URL construction. - HTML trust pipeline. Stream and LLM HTML flows route through a shared trust gate (
packages/web/src/security/html-trust.ts) with three modes:text(default, no HTML),sanitized-html(strips a tag-name blocklist —script,style,iframe,object,embed,svg,math,base,meta,link,noscript,form— plus event-handler attributes,srcdoc/style, andjavascript:/data:text/html/data:text/javascript/data:application/x-javascriptschemes on url-sink attributes (href,src,xlink:href,action,formaction,ping,background,cite,data,poster)), and explicittrusted-html(caller asserts via an opt-in flag, otherwise downgrades tosanitized-html). The DOM morph routes through this same pipeline atpackages/web/src/morph/diff-pure.ts:30(createHtmlFragment(html, { policy: 'sanitized-html' })); there is no third unguardedinnerHTMLpath. Parsing happens via<template>.innerHTML(a non-live, fragment-parser context), which removes the classic mXSS re-serialization vector for thecreateHtmlFragmentpath;resolveHtmlStringdoes re-serialize and so retains a narrower mXSS surface around elements whose serialization differs from their parse (rare;<noscript>is now blocklisted). The sanitizer is bespoke (not DOMPurify) and is exercised bytests/regression/red-team-runtime.test.ts(regression cases including<script>,<iframe>,<embed>,<object>,<svg>,<math>,<style>,<base>,<meta>,<link>,<form>,<noscript>,formaction/action/pingjavascript:schemes, anddata:-variant URI schemes on url-sink attributes); for high-assurance deployments, an independent audit before relying on it as the sole defense is recommended. - Theme/CSS sanitization. Theme compilation (
compileThemeinpackages/edge/src/theme-compiler.ts) rejects unsafe prefixes (e.g. attempts to escape custom-property scoping) and CSS-breaking token values. - Boundary state surface. Boundary state application (
packages/astro/src/runtime/boundary.ts) filters CSS keys to--czap-*and DOM attributes torole/aria-*. Arbitrary attribute injection is rejected at the application layer. - Bootstrap snapshot hardening. The
__CZAP_DETECT__snapshot is non-enumerable, frozen, and intentionally minimal. Astro integration installs the runtime policy in two places: (1) a module-private store insidepackages/astro/src/runtime/policy.tsis the canonical source of truth — a closure no external script can reach viaObject.defineProperty; (2) a frozenwindow.__CZAP_RUNTIME_POLICY__cross-bundle broadcast is published once per realm withconfigurable: false+writable: false, so an attacker who later runs script on the page cannot redefine the global. HMR re-bootstraps and test harnesses update the module-private store; the window broadcast stays locked at first publish. Reads (readRuntimePolicy) check the module-private store first and fall back to the broadcast only for consumers loaded as a separate bundle. - No eval, no new Function. Untrusted text never becomes executable JavaScript at runtime. Verified by grep across
packages/*/src/; the discipline is enforced by code review, not by an ESLint rule today (ano-eval/no-new-funcrule is on the roadmap). WASM bytecode does run at runtime, sandboxed by the host's WASM runtime; the no-WASM fallback (packages/core/src/wasm-fallback.ts) keeps the same kernels available in pure TypeScript.
Runtime code is compatible with strict CSP policies in the sense that LiteShip itself does not call eval or new Function. Deploying under strict CSP still requires the host to make several deliberate decisions, and LiteShip does not abstract them.
script-src: Astro injects bootstrap scripts. The host must add per-request hashes or nonces. There is no built-in nonce-threading API; the host plumbs the nonce into the Astro integration and keeps it consistent with the response header.worker-src: the off-thread compositor, render worker, and audio processor are spawned fromblob:URLs (packages/worker/src/compositor-startup.ts,packages/worker/src/render-worker.ts,packages/web/src/audio/processor-bootstrap.ts). A policy ofworker-src 'self'will silently fail these workers; the host needsworker-src blob:(orworker-src 'self' blob:) for the off-thread paths to start. Note thatblob:is a wildcard for any blob URL the page can construct — including blobs an attacker could create if they have script execution elsewhere. That tradeoff is the cost of the inline-worker bootstrap; if your threat model can't accept it, the alternative is to host the worker scripts under a same-origin path (a follow-up packaging mode that LiteShip doesn't ship today).connect-src: SSE and LLM endpoints. The host's allowlist policy (see "Runtime URL allowlist" above) must agree with the CSPconnect-srclist.
LiteShip writes to innerHTML in two sanctioned places: the templated HTML-fragment helper at packages/web/src/security/html-trust.ts (createHtmlFragment, used by the DOM morph and slot-injection paths) and the LLM session HTML sink at packages/astro/src/runtime/llm-session.ts. Both are gated by the shared trust pipeline (text / sanitized-html / explicit trusted-html), but both are still raw innerHTML assignments. Under Trusted Types enforcement those assignments throw unless the host installs a TrustedHTML policy.
As of this version, the runtime itself routes innerHTML writes through a czap Trusted Types policy when window.trustedTypes is available (packages/web/src/security/html-trust.ts). If the host has not pre-installed a czap policy, the runtime creates a passthrough one on first use — sanitization still runs upstream of the policy callback, so the policy is the Trusted Types attestation, not a second sanitizer.
A host can pre-install a stricter policy if it wants additional belt-and-suspenders behavior:
// In your application bootstrap, before any LiteShip runtime code runs:
if (window.trustedTypes && window.trustedTypes.createPolicy) {
window.trustedTypes.createPolicy('czap', {
// Input has already been sanitized by the LiteShip trust pipeline at this
// point (sanitized-html mode); add additional checks here only if you
// want defense-in-depth on top of the upstream sanitizer.
createHTML: (input) => input,
});
}Important caveat: under the 'trusted-html' policy (caller opted in via allowTrustedHtml: true), the sanitizer is skipped — the host's policy callback receives raw caller-asserted markup. If your host installs a passthrough czap policy and a downstream caller opts into 'trusted-html', you have effectively zero filtering. Either keep callers off 'trusted-html', or have your host policy callback do a second sanitize pass for that path.
If the host enforces Trusted Types via the require-trusted-types-for 'script' CSP directive, the runtime's policy lookup picks up the host policy automatically. No bootstrap step required for the sanitized-html and text paths.
- LiteShip itself: no
eval, nonew Function. Verified acrosspackages/*/src/(no production runtime path uses them). The discipline is enforced by code review, not by an ESLint rule today; a follow-up to addno-evalandno-new-funcESLint rules is on the roadmap. - LiteShip itself: when
window.trustedTypesis available, the runtime looks up (or creates a passthrough)czappolicy automatically. Hosts with stricter requirements pre-install their own. - LiteShip itself: no auto-set CSP. The host owns the policy.
When hosting on Cloudflare Workers via @czap/cloudflare:
- CSP: Same browser CSP requirements as above apply to HTML responses served from the Worker.
worker-src 'self' blob:remains required forclient:workeroff-thread paths in the browser. - Cross-origin isolation: If you enable
workers: { enabled: true }in@czap/astro, emitCross-Origin-Opener-Policy: same-originandCross-Origin-Embedder-Policy: require-corpon HTML responses (seepackages/astro/src/headers.ts). Without these headers,SharedArrayBufferand full off-thread worker paths are unavailable in the browser. - KV trust boundary: Workers KV is a host-controlled cache for boundary compile outputs, not a secrets store. Bindings are declared in
wrangler.jsoncand accessed viacloudflare:workersenv. See HOSTING.md.
The repo includes a dedicated red-team regression lane:
pnpm run test:redteamIt runs as the red-team gate in pnpm run gauntlet:full (the canonical phase sequence lives in packages/cli/src/gauntlet-phases.ts; czap gauntlet --dry-run projects the live order) and on every PR through .github/workflows/ci.yml. New security-relevant findings should be encoded as a regression test there to prevent re-introduction.
For accepted vulnerabilities:
- We acknowledge receipt (≤ 72 hours).
- We confirm reproducibility and assess severity (≤ 7 days). Severity uses CVSS 3.1 base score bands: Critical 9.0–10.0, High 7.0–8.9, Medium 4.0–6.9, Low 0.1–3.9. Pre-1.0 means we will sometimes resolve a finding with a breaking change instead of a workaround; we'll communicate which path before we ship.
- We ship a fix or mitigation on a severity-keyed timeline:
- Critical: ≤ 7 days
- High: ≤ 14 days
- Medium: ≤ 30 days
- Low: addressed in the next regular release cycle
- We publish a GitHub Security Advisory crediting the reporter (unless they request anonymity), and add a regression to
tests/regression/red-team-runtime.test.tsso the same shape can't re-enter without a test failure.
For findings we determine are not vulnerabilities (e.g. expected behavior, out-of-scope, host-application responsibility), we'll explain that reasoning in the advisory thread.
In scope:
- All packages under
@czap/* - The Vite plugin, Astro integration, and edge host adapters
- The capsule factory and content-addressing primitives
- The runtime trust gate and HTML sanitization paths
Out of scope (not because they don't matter, but because they're not LiteShip's published security surface):
- Vulnerabilities in upstream dependencies (please report to those maintainers; we'll bump if a fix is available)
- Misconfiguration in consumer applications (e.g. a host app disabling the trust gate intentionally)
- Issues in third-party tooling (Vite, Astro, Playwright, vitest)