Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion sdk/typescript/src/trusted-executable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ export async function resolveTrustedExecutable(
process.platform === "win32" ? constants.F_OK : constants.X_OK,
);
if (!(await stat(canonical)).isFile()) continue;
executable ??= pathLike ? canonical : current.path;
// Explicit launchers outside the protected root retain invocation semantics
// such as Python virtualenv selection. Repository-local links still execute
// only the canonical target that passed the trust check.
executable ??=
pathLike && isWithin(root, current.path) ? canonical : current.path;
Comment on lines +71 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Canonicalize aliased repository launchers

When an explicit path reaches a repository-local launcher through an alias outside the protected root (for example, protectedRoot=/real/repo and /tmp/repo-alias/python, where repo-alias points to that repository), current.path appears outside root even though its parent is repository-controlled. If python initially links to a trusted system interpreter, validation succeeds and this branch returns the mutable repository path; retargeting that link after the probe causes subsequent plugin invocations to execute repository code. Canonicalize the launcher's parent before deciding whether its invocation path is outside the protected root.

Useful? React with 👍 / 👎.

} catch {
continue;
}
Expand Down
30 changes: 29 additions & 1 deletion sdk/typescript/tests-ts/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2809,6 +2809,34 @@ describe("runtime directories and plugin Python boundary", () => {
).rejects.toThrow(PluginPythonUnavailableError);
});

testPosix("preserves an explicit virtualenv Python launcher", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const systemBin = join(root, "system", "bin");
const virtualenvBin = join(root, "venv", "bin");
const systemPython = join(systemBin, "python3");
const virtualenvPython = join(virtualenvBin, "python");
await Promise.all([
mkdir(repository),
mkdir(systemBin, { recursive: true }),
mkdir(virtualenvBin, { recursive: true }),
]);
await writeFile(
systemPython,
'#!/bin/sh\ncase "$0" in */venv/bin/python) ;; *) exit 1 ;; esac\nprintf "codex-security-python-ok\\n"\n',
);
await chmod(systemPython, 0o700);
await symlink(systemPython, virtualenvPython);

await expect(
resolvePluginPython({
configuredPath: virtualenvPython,
environment: { PATH: "" },
protectedRoot: repository,
}),
).resolves.toBe(virtualenvPython);
});

testPosix(
"does not load repository-controlled Python startup code",
async () => {
Expand Down Expand Up @@ -2838,7 +2866,7 @@ describe("runtime directories and plugin Python boundary", () => {
environment,
protectedRoot: repository,
}),
).toBe(await realpath(interpreter));
).toBe(interpreter);
expect(existsSync(marker)).toBe(false);
},
);
Expand Down