fix(codex): close parent-dir symlink escape in the containment check

This commit is contained in:
2026-09-08 17:43:09 +07:00
parent f3a2f41ed7
commit 58623f37c0
2 changed files with 127 additions and 35 deletions
+59
View File
@@ -207,6 +207,65 @@ describe("codex-mutate", () => {
fs.rmSync(outside, { recursive: true, force: true });
});
it("refuses a brand-new artifact whose subdir (not the leaf) is a symlink escaping the root", () => {
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-outside-"));
const linkPath = path.join(TMP, "agents");
// "agents" doesn't exist yet in this fresh suite of tests for this scope —
// remove it first if an earlier test already created it as a real dir.
fs.rmSync(linkPath, { recursive: true, force: true });
fs.symlinkSync(outside, linkPath, "dir");
assert.throws(
() =>
cm.writeArtifact({
scope: "user",
type: "agents",
name: "brand-new",
content: "pwned",
cwd: REPO,
}),
(e) => e.code === "EOUTOFROOT"
);
assert.equal(fs.existsSync(path.join(outside, "brand-new.toml")), false);
fs.rmSync(linkPath, { force: true });
fs.rmSync(outside, { recursive: true, force: true });
});
it("still writes correctly when $CODEX_HOME itself is a symlink (a dotfiles checkout)", () => {
const realHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-realhome-"));
const linkHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-mut-linkhome-"));
fs.rmdirSync(linkHome); // symlinkSync needs the link path itself to not exist
fs.symlinkSync(realHome, linkHome, "dir");
const prev = process.env.CODEX_HOME;
process.env.CODEX_HOME = linkHome;
delete require.cache[require.resolve("../lib/codex-home")];
delete require.cache[require.resolve("../lib/codex-mutate")];
const linkedCm = require("../lib/codex-mutate");
try {
const { file, created } = linkedCm.writeArtifact({
scope: "user",
type: "rules",
name: "via-link",
content: "ok",
cwd: REPO,
});
assert.equal(created, true);
assert.equal(fs.readFileSync(file, "utf8"), "ok");
assert.equal(
fs.readFileSync(path.join(realHome, "rules", "via-link.rules"), "utf8"),
"ok",
"the write must land in the real directory the symlinked CODEX_HOME points to"
);
} finally {
process.env.CODEX_HOME = prev;
delete require.cache[require.resolve("../lib/codex-home")];
delete require.cache[require.resolve("../lib/codex-mutate")];
fs.rmSync(linkHome, { force: true });
fs.rmSync(realHome, { recursive: true, force: true });
}
});
for (const badName of ["..", ".", "/etc/passwd", "a/b", "a\0b"]) {
it(`rejects the bad name ${JSON.stringify(badName)} with EBADNAME and writes nothing`, () => {
const target = path.join(TMP, "agents", `${badName.replace(/[/\0]/g, "_")}.toml`);