fix(codex): close parent-dir symlink escape in the containment check
This commit is contained in:
@@ -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`);
|
||||
|
||||
Reference in New Issue
Block a user