fix(codex): close symlink escape on GET /file, pin project writes to server cwd
This commit is contained in:
@@ -199,6 +199,10 @@ async function mcpAdd({ name, url, bearerTokenEnvVar, command, args = [], env })
|
||||
if (!url && !command) {
|
||||
throw makeError("EBADARG", "mcpAdd requires either url or command");
|
||||
}
|
||||
if (!Array.isArray(args)) throw makeError("EBADARG", "args must be an array");
|
||||
if (env !== undefined && (typeof env !== "object" || env === null || Array.isArray(env))) {
|
||||
throw makeError("EBADARG", "env must be an object");
|
||||
}
|
||||
|
||||
if (url) {
|
||||
if (env) throw makeError("EBADARG", "env is not valid together with url (stdio-only)");
|
||||
@@ -256,8 +260,26 @@ async function marketplaceRemove(name) {
|
||||
return serialize(() => runCodex(["plugin", "marketplace", "remove", n]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort human message from a failed run. The CLI prints its errors with
|
||||
* a `✘` marker on either stream; fall back to the last non-empty line.
|
||||
* Unchanged copy of cc-plugin-cli.js's cliMessage — same CLI-output shape,
|
||||
* same heuristic.
|
||||
*/
|
||||
function cliMessage(res) {
|
||||
const text = `${res.stderr || ""}\n${res.stdout || ""}`;
|
||||
const lines = text
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean);
|
||||
const marked = lines.find((l) => l.includes("✘"));
|
||||
const picked = marked || lines[lines.length - 1] || "";
|
||||
return picked.replace(/^✘\s*/, "").slice(0, 1000);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
runCodex,
|
||||
cliMessage,
|
||||
mcpList,
|
||||
mcpAdd,
|
||||
mcpRemove,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -300,6 +300,7 @@ module.exports = {
|
||||
deleteArtifact,
|
||||
listBackups,
|
||||
resolveTarget, // exported for tests
|
||||
assertNotSymlinkEscape, // shared with codex-discovery.js's readFileSafe — one check, both verbs
|
||||
TYPES,
|
||||
NAME_RE,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user