From bacacbfc8d546049554afcff0195af5c4e520bbc Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:17:13 +0100 Subject: [PATCH 1/2] fix(contract): constrain code-evidence paths --- sdk/typescript/_bundled_plugin/schemas/findings.schema.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/_bundled_plugin/schemas/findings.schema.json b/sdk/typescript/_bundled_plugin/schemas/findings.schema.json index b282914da..f834d2d29 100644 --- a/sdk/typescript/_bundled_plugin/schemas/findings.schema.json +++ b/sdk/typescript/_bundled_plugin/schemas/findings.schema.json @@ -234,7 +234,8 @@ }, "path": { "type": "string", - "minLength": 1 + "minLength": 1, + "pattern": "^(?!/)(?!\\.$)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*\\\\)(?!.*:)(?!.*\\u0000).+$" }, "startLine": { "type": "integer", From 4c356cd128f3528cf89f131e00ab08f1f50dc90c Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Tue, 18 Aug 2026 08:17:50 +0100 Subject: [PATCH 2/2] test(contract): reject unsafe code-evidence paths --- .../tests-ts/code-evidence-paths.test.ts | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 sdk/typescript/tests-ts/code-evidence-paths.test.ts diff --git a/sdk/typescript/tests-ts/code-evidence-paths.test.ts b/sdk/typescript/tests-ts/code-evidence-paths.test.ts new file mode 100644 index 000000000..576b3b156 --- /dev/null +++ b/sdk/typescript/tests-ts/code-evidence-paths.test.ts @@ -0,0 +1,95 @@ +import { createHash } from "node:crypto"; +import { chmod, cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "bun:test"; +import { loadContract } from "../src/index.js"; +import { PLUGIN_ROOT } from "./plugin-root.js"; + +const EXAMPLE = join(PLUGIN_ROOT, "examples", "completed-scan"); +const temporaryDirectories: string[] = []; + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((path) => rm(path, { recursive: true, force: true })), + ); +}); + +async function scanWithEvidencePath(path: string): Promise { + const root = await mkdtemp(join(tmpdir(), "codex-security-evidence-path-")); + temporaryDirectories.push(root); + const scanDir = join(root, "scan"); + await cp(EXAMPLE, scanDir, { recursive: true }); + if (process.platform !== "win32") await chmod(scanDir, 0o700); + + const findingsPath = join(scanDir, "findings.json"); + const findings = JSON.parse(await readFile(findingsPath, "utf8")) as { + findings: Array>; + }; + findings.findings[0]!["codeEvidence"] = [ + { + id: "evidence-1", + label: "Source evidence", + path, + startLine: 41, + endLine: 44, + language: "python", + role: "sink", + code: "target.write(data)", + explanation: "The selected path reaches the filesystem write.", + }, + ]; + await writeFile(findingsPath, `${JSON.stringify(findings, null, 2)}\n`); + + const manifestPath = join(scanDir, "scan-manifest.json"); + const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as { + scan: { artifacts: Array<{ path: string; sha256: string }> }; + }; + const artifact = manifest.scan.artifacts.find( + (candidate) => candidate.path === "findings.json", + ); + expect(artifact).toBeDefined(); + artifact!.sha256 = createHash("sha256") + .update(await readFile(findingsPath)) + .digest("hex"); + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + return scanDir; +} + +describe("canonical code-evidence paths", () => { + test("rejects paths outside the repository-relative POSIX boundary", async () => { + for (const path of [ + "../../outside.ts", + "/etc/passwd", + "C:/outside.ts", + "src\\outside.ts", + ".", + "src:stream.ts", + "src/\0outside.ts", + ]) { + const scanDir = await scanWithEvidencePath(path); + await expect( + loadContract(scanDir, { pluginRoot: PLUGIN_ROOT }), + ).rejects.toThrow("findings.json"); + } + }); + + test("accepts a repository-relative code-evidence path", async () => { + const scanDir = await scanWithEvidencePath("src/extract.py"); + await expect( + loadContract(scanDir, { pluginRoot: PLUGIN_ROOT }), + ).resolves.toMatchObject({ + findings: { + findings: [ + { + codeEvidence: [ + expect.objectContaining({ path: "src/extract.py" }), + ], + }, + ], + }, + }); + }); +});