diff --git a/packages/kilo-vscode/src/utils.ts b/packages/kilo-vscode/src/utils.ts index 72beab466da..948c20e2ee4 100644 --- a/packages/kilo-vscode/src/utils.ts +++ b/packages/kilo-vscode/src/utils.ts @@ -1,5 +1,6 @@ import * as crypto from "crypto" import * as vscode from "vscode" +import { buildCspString } from "./webview-html-utils" export function getNonce(): string { return crypto.randomBytes(16).toString("hex") @@ -17,18 +18,7 @@ export function buildWebviewHtml( }, ): string { const nonce = getNonce() - const connectSrc = opts.port - ? `http://127.0.0.1:${opts.port} http://localhost:${opts.port} ws://127.0.0.1:${opts.port} ws://localhost:${opts.port}` - : "http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*" - - const csp = [ - "default-src 'none'", - `style-src 'unsafe-inline' ${webview.cspSource}`, - `script-src 'nonce-${nonce}' 'wasm-unsafe-eval'`, - `font-src ${webview.cspSource}`, - `connect-src ${connectSrc}`, - `img-src ${webview.cspSource} data: https:`, - ].join("; ") + const csp = buildCspString(webview.cspSource, nonce, opts.port) return ` diff --git a/packages/kilo-vscode/src/webview-html-utils.ts b/packages/kilo-vscode/src/webview-html-utils.ts new file mode 100644 index 00000000000..629320dfa06 --- /dev/null +++ b/packages/kilo-vscode/src/webview-html-utils.ts @@ -0,0 +1,34 @@ +/** + * Build the Content-Security-Policy connect-src directive value. + * If a port is specified, restricts connections to that port. + * Otherwise, allows any localhost/127.0.0.1 port. + */ +export function buildConnectSrc(port?: number): string { + if (port) { + return `http://127.0.0.1:${port} http://localhost:${port} ws://127.0.0.1:${port} ws://localhost:${port}` + } + return "http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*" +} + +/** + * Join an array of CSP directives into a policy string. + */ +export function joinCspDirectives(directives: string[]): string { + return directives.join("; ") +} + +/** + * Build the full CSP policy string for a webview. + */ +export function buildCspString(cspSource: string, nonce: string, port?: number): string { + const connectSrc = buildConnectSrc(port) + const directives = [ + "default-src 'none'", + `style-src 'unsafe-inline' ${cspSource}`, + `script-src 'nonce-${nonce}' 'wasm-unsafe-eval'`, + `font-src ${cspSource}`, + `connect-src ${connectSrc}`, + `img-src ${cspSource} data: https:`, + ] + return joinCspDirectives(directives) +} diff --git a/packages/kilo-vscode/tests/unit/webview-html.test.ts b/packages/kilo-vscode/tests/unit/webview-html.test.ts new file mode 100644 index 00000000000..2b930f163fb --- /dev/null +++ b/packages/kilo-vscode/tests/unit/webview-html.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect } from "bun:test" +import { buildConnectSrc, buildCspString } from "../../src/webview-html-utils" + +describe("buildConnectSrc", () => { + it("uses wildcard ports when no port specified", () => { + const result = buildConnectSrc() + expect(result).toContain("http://127.0.0.1:*") + expect(result).toContain("http://localhost:*") + expect(result).toContain("ws://127.0.0.1:*") + expect(result).toContain("ws://localhost:*") + }) + + it("restricts to specific port when port provided", () => { + const result = buildConnectSrc(3000) + expect(result).toContain("http://127.0.0.1:3000") + expect(result).toContain("http://localhost:3000") + expect(result).toContain("ws://127.0.0.1:3000") + expect(result).toContain("ws://localhost:3000") + }) + + it("does not include wildcard when port is provided", () => { + const result = buildConnectSrc(3000) + expect(result).not.toContain(":*") + }) + + it("uses the exact port number", () => { + expect(buildConnectSrc(54321)).toContain(":54321") + }) +}) + +describe("buildCspString", () => { + const cspSource = "vscode-resource://test" + const nonce = "abc123" + + it("includes default-src 'none'", () => { + expect(buildCspString(cspSource, nonce)).toContain("default-src 'none'") + }) + + it("includes nonce in script-src", () => { + const result = buildCspString(cspSource, nonce) + expect(result).toContain(`'nonce-${nonce}'`) + expect(result).toContain("'wasm-unsafe-eval'") + }) + + it("includes cspSource in style-src and font-src", () => { + const result = buildCspString(cspSource, nonce) + expect(result).toContain(`style-src 'unsafe-inline' ${cspSource}`) + expect(result).toContain(`font-src ${cspSource}`) + }) + + it("includes cspSource and https: in img-src", () => { + const result = buildCspString(cspSource, nonce) + expect(result).toContain("img-src") + expect(result).toContain(cspSource) + expect(result).toContain("https:") + expect(result).toContain("data:") + }) + + it("uses wildcard connect-src when no port provided", () => { + const result = buildCspString(cspSource, nonce) + expect(result).toContain("http://127.0.0.1:*") + }) + + it("uses specific port in connect-src when port provided", () => { + const result = buildCspString(cspSource, nonce, 9000) + expect(result).toContain("http://127.0.0.1:9000") + expect(result).not.toContain(":*") + }) + + it("joins directives with semicolons", () => { + const result = buildCspString(cspSource, nonce) + const parts = result.split(";") + expect(parts.length).toBeGreaterThanOrEqual(5) + }) +})