diff --git a/sdk/typescript/src/targets.ts b/sdk/typescript/src/targets.ts index 5c8ab088..c13227d2 100644 --- a/sdk/typescript/src/targets.ts +++ b/sdk/typescript/src/targets.ts @@ -276,6 +276,20 @@ export async function normalizeTarget( `Path target is outside the repository: ${value}`, ); } + let metadata; + try { + metadata = await abortable(() => stat(canonical), signal); + } catch (error) { + throwIfAborted(signal); + throw new InvalidTargetError(`Path target does not exist: ${value}`, { + cause: error, + }); + } + if (!metadata.isFile() && !metadata.isDirectory()) { + throw new InvalidTargetError( + `Path target must be a regular file or directory: ${value}`, + ); + } const normalized = relativePath.split(sep).join("/") || "."; if (!paths.includes(normalized)) { paths.push(normalized); diff --git a/sdk/typescript/tests-ts/path-target-special-files.test.ts b/sdk/typescript/tests-ts/path-target-special-files.test.ts new file mode 100644 index 00000000..f2cab162 --- /dev/null +++ b/sdk/typescript/tests-ts/path-target-special-files.test.ts @@ -0,0 +1,46 @@ +import { createServer } from "node:net"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { expect, test } from "bun:test"; +import { normalizeTarget } from "../src/targets.js"; + +test.skipIf(process.platform === "win32")( + "rejects special filesystem nodes during path-target preflight", + async () => { + const root = await mkdtemp(join(tmpdir(), "codex-security-special-target-")); + const repository = join(root, "repository"); + const socketPath = join(repository, "target.sock"); + await mkdir(repository); + const server = createServer(); + + try { + await new Promise((resolvePromise, reject) => { + server.once("error", reject); + server.listen(socketPath, () => resolvePromise()); + }); + + await expect(normalizeTarget(repository, [socketPath])).rejects.toThrow( + "Path target must be a regular file or directory", + ); + } finally { + await new Promise((resolvePromise) => server.close(() => resolvePromise())); + await rm(root, { recursive: true, force: true }); + } + }, +); + +test("keeps ordinary file and directory path targets valid", async () => { + const root = await mkdtemp(join(tmpdir(), "codex-security-regular-target-")); + const repository = join(root, "repository"); + try { + await mkdir(join(repository, "src"), { recursive: true }); + await writeFile(join(repository, "src", "app.ts"), "export {};\n"); + + await expect( + normalizeTarget(repository, ["src/app.ts", "src"]), + ).resolves.toEqual({ kind: "paths", paths: ["src/app.ts", "src"] }); + } finally { + await rm(root, { recursive: true, force: true }); + } +});