fix(codex): close symlink escape on GET /file, pin project writes to server cwd

This commit is contained in:
2026-09-08 20:07:57 +07:00
parent 32839332f2
commit 29ed86d672
7 changed files with 246 additions and 89 deletions
+48
View File
@@ -22,6 +22,11 @@ const {
isFileLike,
MAX_FILE_BYTES,
} = require("./cc-discovery");
// Shared symlink-escape guard — the same check writeArtifact/deleteArtifact
// use, so a read and a write of the same path agree on what "inside the
// root" means. See codex-mutate.js's assertNotSymlinkEscape for the leaf +
// ancestor symlink reasoning.
const { assertNotSymlinkEscape } = require("./codex-mutate");
function safeReadText(absPath) {
try {
@@ -286,6 +291,48 @@ function readOverview(opts = {}) {
};
}
// ── Single-file body endpoint (with strict path containment) ───────────
/**
* Read the body of one file living under CODEX_HOME, the project .codex
* dir, or (only for a file literally named AGENTS.md) the project root.
* Mirrors cc-discovery's readFileSafe, plus the same leaf/ancestor
* symlink-escape guard the write side (codex-mutate.js) uses — a string-only
* `isUnder` check alone would let a symlink dropped anywhere along the path
* read arbitrary files outside the root.
*/
function readFileSafe(absPath, opts = {}) {
const codexHome = getCodexHome();
const projectCodexDir = getProjectCodexDir(opts.cwd);
const projectRoot = getProjectRoot(opts.cwd);
const resolved = path.resolve(absPath);
const inside = [codexHome, projectCodexDir, projectRoot].some((root) => isUnder(root, resolved));
if (!inside) return { error: "path is outside allowed roots" };
if (
isUnder(projectRoot, resolved) &&
!isUnder(projectCodexDir, resolved) &&
path.basename(resolved) !== "AGENTS.md"
) {
return { error: "only AGENTS.md is readable from project root" };
}
const trustedRoot = isUnder(codexHome, resolved)
? codexHome
: isUnder(projectCodexDir, resolved)
? projectCodexDir
: projectRoot;
try {
assertNotSymlinkEscape(trustedRoot, resolved);
} catch (err) {
return { error: err.message };
}
const r = safeReadText(resolved);
if (!r) return { error: "file not readable" };
return { ok: true, file: resolved, ...r };
}
module.exports = {
readSkills,
readAgents,
@@ -293,5 +340,6 @@ module.exports = {
readMemory,
readConfig,
readOverview,
readFileSafe,
readTomlMeta,
};