fix(desktop): protect packaged renderer scripts

This commit is contained in:
Hona 2026-08-31 17:01:38 +00:00 committed by opencode-agent[bot]
parent ec0dcb3da9
commit c7ca52286a
3 changed files with 39 additions and 11 deletions

View file

@ -0,0 +1,16 @@
import { expect, test } from "bun:test"
import { addDocumentHeaders } from "./document-headers"
test("protects packaged renderer documents", () => {
const response = addDocumentHeaders(new Response("app"), "/renderer/index.html")
expect(response.headers.get("content-security-policy")).toContain("script-src 'self' 'wasm-unsafe-eval'")
expect(response.headers.get("content-security-policy")).toContain("object-src 'none'")
expect(response.headers.get("content-security-policy")).toContain("base-uri 'none'")
expect(response.headers.get("document-policy")).toBe("include-js-call-stacks-in-crash-reports")
})
test("leaves renderer assets unchanged", () => {
const response = new Response("script")
expect(addDocumentHeaders(response, "/renderer/app.js")).toBe(response)
})

View file

@ -0,0 +1,20 @@
const contentSecurityPolicy = [
"default-src 'self'",
"script-src 'self' 'wasm-unsafe-eval'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https: blob:",
"font-src 'self' data:",
"media-src 'self' data:",
"connect-src * data: blob:",
"worker-src 'self' blob:",
"object-src 'none'",
"base-uri 'none'",
].join("; ")
export function addDocumentHeaders(response: Response, file: string) {
if (!file.toLowerCase().endsWith(".html")) return response
const headers = new Headers(response.headers)
headers.set("Content-Security-Policy", contentSecurityPolicy)
headers.set("Document-Policy", "include-js-call-stacks-in-crash-reports")
return new Response(response.body, { status: response.status, statusText: response.statusText, headers })
}

View file

@ -4,11 +4,10 @@ import { pathToFileURL } from "node:url"
import { Effect, Path } from "effect"
import { scoped } from "../native/logging"
import { DesktopPaths } from "../paths"
import { addDocumentHeaders } from "./document-headers"
const rendererProtocol = "oc"
const rendererHost = "renderer"
const documentPolicyHeader = "Document-Policy"
const jsCallStacksDocumentPolicy = "include-js-call-stacks-in-crash-reports"
protocol.registerSchemesAsPrivileged([
{
@ -58,7 +57,7 @@ export const registerRendererProtocol = Effect.fn("Window.registerRendererProtoc
),
)
}
return addDocumentPolicy(response, file)
return addDocumentHeaders(response, file)
} catch (error) {
runFork(scoped("protocol", Effect.logError("fetch error", { url: request.url, file, error })))
return new Response("Not found", { status: 404 })
@ -88,17 +87,10 @@ export function isRendererUrl(value?: string, html = false) {
export function addRendererHeaders(value: string, headers: object) {
upsertHeader(headers, "Access-Control-Allow-Origin", ["*"])
upsertHeader(headers, "Access-Control-Allow-Headers", ["*"])
if (isRendererUrl(value, true)) upsertHeader(headers, documentPolicyHeader, [jsCallStacksDocumentPolicy])
if (isRendererUrl(value, true)) upsertHeader(headers, "Document-Policy", ["include-js-call-stacks-in-crash-reports"])
}
export function upsertHeader(headers: object, key: string, value: string | string[]) {
const current = Object.keys(headers).find((header) => header.toLowerCase() === key.toLowerCase())
Reflect.set(headers, current ?? key, value)
}
function addDocumentPolicy(response: Response, file: string) {
if (!file.toLowerCase().endsWith(".html")) return response
const headers = new Headers(response.headers)
headers.set(documentPolicyHeader, jsCallStacksDocumentPolicy)
return new Response(response.body, { status: response.status, statusText: response.statusText, headers })
}