安全运行时契约

Secure runtime 决定了不可信 Slex source 能做什么、不能做什么,宿主如何授权能力,以及 sandbox isolation 如何工作。

威胁模型

Secure mode 隔离 expression execution,敏感能力收敛到 host policyapi.*

授权来源

只有宿主提供的 HostRuntimePolicy 可以授权能力。Slex source 中的 capabilitiespermissionsapi 或其他 top-level declarations 都不能自我授权。

所有能力通过 api.* 访问:

api.get(url, options)
api.post(url, body, options)
api.fetch(url, options)
api.setTimeout(fn, ms)
api.clearTimeout(id)
api.setInterval(fn, ms)
api.clearInterval(id)
api.raf(fn)
api.cancelRaf(id)
api.createCanvas(width, height)
api.getCanvasContext(canvas, contextId, options)
api.onDispose(fn)
api.now()
api.isTimeoutError(error)
api.isNetworkError(error)
api.isPolicyError(error)
api.errorMessage(error)

没有通过 api.* 暴露的能力都不受支持。

HostRuntimePolicy

type HostRuntimePolicy = {
  network?: {
    enabled: boolean;
    methods: ("GET" | "POST")[];
    allowOrigins: string[];
    allowHeaders?: string[];
    allowContentTypes?: string[];
    credentials: "omit" | "same-origin" | "include";
    timeoutMs: number;
    maxBodyBytes: number;
    maxResponseBytes?: number;
  };
  timer?: {
    enabled: boolean;
    maxTimers: number;
    minIntervalMs: number;
  };
  animation?: { enabled: boolean };
  canvas?: {
    enabled: boolean;
    maxCanvases?: number;
    maxPixels?: number;
    allowedContexts?: ("2d" | "webgl" | "webgl2" | "bitmaprenderer")[];
  };
  execution?: {
    heartbeatIntervalMs?: number;
    maxUnresponsiveMs?: number;
  };
};

网络策略

Network 默认拒绝,除非 policy.network.enabledtrue。Policy 限制:

AuthorizationCookieProxy-AuthorizationSet-Cookie 和 Sec-Fetch headers 始终阻断。hostAdapter.fetch 可替换实际请求实现;hostAdapter.onNetworkLog 只能观察,不能改变 runtime 行为。

定时器、动画与 canvas

执行监控

execution.heartbeatIntervalMs 控制 sandbox heartbeat 频率。execution.maxUnresponsiveMs 定义最大静默时间,超过后 sandbox 会被视为 unresponsive 并终止。

HostRuntimeAdapter

Adapter 允许宿主替换或观察部分 runtime behavior:

type HostRuntimeAdapter = {
  fetch?: (request: HostFetchRequest) => Promise<NetworkResult>;
  onNetworkLog?: (event: RuntimeNetworkLogEvent) => void;
  onRuntimeError?: (event: RuntimeErrorEvent) => void;
  now?: () => number;
  setTimeout?: (fn: () => void, ms: number) => TimerId;
  clearTimeout?: (id: TimerId) => void;
  setInterval?: (fn: () => void, ms: number) => TimerId;
  clearInterval?: (id: TimerId) => void;
  requestAnimationFrame?: (fn: (time: number) => void) => RafId;
  cancelAnimationFrame?: (id: RafId) => void;
};

onNetworkLogonRuntimeError 是 audit hooks,不能改变 runtime behavior;内部抛错会被静默捕获。

Sandbox iframe 部署

Secure frame 从 runtimeUrl 导入主 runtime module:

mountSecureArtifact(script, container, {
  frame: {
    runtimeUrl: "/slexkit.runtime.js"
  }
});

这个 URL 必须作为 public ES module 提供,并返回:

Access-Control-Allow-Origin: *
Content-Type: text/javascript

这是 server 或 deployment layer 配置,不能由 frontend JavaScript 事后设置。

CSP

Sandbox iframe 通过 srcdoc 创建,并使用严格 Content-Security-Policy:

default-src 'none'
script-src 'nonce-{random}' 'unsafe-eval' {runtimeOrigin}
connect-src 'none'
img-src data: blob:
style-src 'unsafe-inline'
font-src data:
form-action 'none'
base-uri 'none'

每个 frame instance 生成随机 nonce。unsafe-eval 是必要的,因为 Slex expression evaluation 在 sandbox 内使用 eval。

Sandbox attribute

iframe 需要 allow-scriptsallow-same-origin 默认 blocked,因为它会削弱 opaque-origin isolation。只有宿主显式接受风险时,才使用 unsafeAllowSameOrigin: true

frame: {
  unsafeAllowSameOrigin: true,
  sandbox: "allow-scripts allow-same-origin"
}

不要为了修复 CORS 或调试问题添加 allow-same-origin

postMessage bridge 协议

宿主和 sandbox 通过 window.postMessage 通信。所有消息都带有 channel: "slexkit-secure"

宿主到 sandbox 消息

类型 用途
mount 发送 Slex source、policy、theme
dispose 通知 sandbox teardown
fetch-result 返回 fetch response 或 error
slots 同步 artifact slot positions

Sandbox 到宿主消息

类型 用途
ready Runner module 已加载并监听
mounted Artifact 渲染完成
disposed Sandbox 清理已确认
heartbeat 心跳信号
error mount 或 runtime error
fetch 请求宿主代发网络访问
slot-size artifact slot 高度报告

每条 host-to-sandbox message 包含 idtoken。Token 是 opaque、cryptographically random,并绑定到单个 mount instance。Token 不匹配的消息会被拒绝。

Sandbox 验证 event.source === window.parent;host 验证 event.source === iframe.contentWindow

Artifact slot 桥接

属于同一 artifact 的多个 Markdown fences 共享一个 sandbox iframe。Host 把 slot rectangles 发送给 sandbox;sandbox 在对应 slot container 中渲染每个 fence 的输出,并通过 slot-size 回传高度。

Host 和 sandbox 两侧的 ResizeObserver 保持位置与高度同步。这让多个 fence 之间保持视觉连续,同时所有执行都限制在一个隔离上下文中。

Heartbeat 看门狗

配置 execution.maxUnresponsiveMs 后,host 会监控 sandbox heartbeat。若超过阈值没有 heartbeat,iframe 会被终止,页面渲染 role="alert" diagnostic,并输出 console.error

Fail-closed 行为

如果 iframe 无法加载 runtime、没有发送 ready/mounted message,或 heartbeat timeout,SlexKit 会:

  1. 移除无响应 iframe。
  2. 渲染包含失败描述的 role="alert" diagnostic element。
  3. 通过 console.error 输出同样信息。

Load timeout 可通过 frame.loadTimeoutMs 配置,默认 8000ms。

逃生开关

unsafeInlineExecution

允许 secure artifact 在宿主页内以内联方式执行,并注入 secure runtime API。只用于测试或 host-trusted content,不推荐用于 untrusted paths。

unsafeAllowSameOrigin

允许 sandbox attribute 中的 allow-same-origin。它会降低隔离强度,只能在宿主明确接受风险时使用。

frame: {
  sandbox: "allow-scripts allow-same-origin",
  unsafeAllowSameOrigin: true
}

Sandbox 加固

Sandbox runner 启动时会 harden global scope:

维护原则