mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-26 18:11:24 +08:00
fix(security): contain install-state file ops to trusted root — RCE fix (GHSA-hfpv-w6mp-5g95)
Critical: project-local install-state (e.g. a cloned repo's .cursor/ecc-install-state.json) is attacker-controllable, and repair/uninstall/auto-update replayed its operations with destinationPath validated only for non-emptiness — confirmed arbitrary file write/delete and chained RCE (write ~/.bashrc, .git/hooks, or run a planted install-apply.js). - New scripts/lib/path-safety.js: assertWithinTrustedRoot() canonicalizes (incl. symlink escape via nearest-existing-ancestor realpath) and fails closed unless the destination is within the adapter-derived trusted root. - install-lifecycle.js: gate executeRepairOperation + executeUninstallOperation + the install-state removal against record.targetRoot (the adapter-resolved root, NOT the attacker-supplied state.target.root). - auto-update.js: validateRepoRoot now requires package.json name to be an official ECC package, so a planted nested repo can't drive auto-update into executing attacker code. - 7 containment regression tests. Existing install-lifecycle/repair/uninstall/auto-update suites still green (legit destinations are within the root).
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Path containment helpers for install-state-driven file operations.
|
||||
*
|
||||
* Install-state files are project-local and therefore attacker-controllable
|
||||
* (a cloned/forked repo can ship a crafted `.cursor/ecc-install-state.json`).
|
||||
* `repair`/`uninstall`/`auto-update` replay recorded operations, so every
|
||||
* write/delete destination MUST be confined to the adapter-derived trusted
|
||||
* root — never trusted from the state file itself (GHSA-hfpv-w6mp-5g95).
|
||||
*/
|
||||
|
||||
function safeRealpath(target) {
|
||||
try {
|
||||
return fs.realpathSync(path.resolve(target));
|
||||
} catch {
|
||||
return path.resolve(target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonicalize a path that may not exist yet: realpath its nearest existing
|
||||
* ancestor, then re-append the missing tail. This defeats symlink escapes
|
||||
* where an intermediate directory is a symlink pointing out of the root.
|
||||
*/
|
||||
function realpathNearestExisting(target) {
|
||||
let current = path.resolve(target);
|
||||
const tail = [];
|
||||
while (!fs.existsSync(current)) {
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
tail.unshift(path.basename(current));
|
||||
current = parent;
|
||||
}
|
||||
const real = safeRealpath(current);
|
||||
return tail.length > 0 ? path.join(real, ...tail) : real;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when `target` resolves to `root` itself or a path beneath it, with
|
||||
* symlinks resolved on both sides.
|
||||
*/
|
||||
function isWithinRoot(target, root) {
|
||||
if (!root) {
|
||||
return false;
|
||||
}
|
||||
const realRoot = safeRealpath(root);
|
||||
const realTarget = realpathNearestExisting(target);
|
||||
if (realTarget === realRoot) {
|
||||
return true;
|
||||
}
|
||||
const rel = path.relative(realRoot, realTarget);
|
||||
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail-closed guard: throw unless `target` is contained within `root`.
|
||||
* Returns the canonicalized target path on success.
|
||||
*/
|
||||
function assertWithinTrustedRoot(target, root, action = 'write') {
|
||||
if (!target || typeof target !== 'string') {
|
||||
throw new Error(`Refusing to ${action}: missing destination path.`);
|
||||
}
|
||||
if (!root) {
|
||||
throw new Error(`Refusing to ${action} '${target}': no trusted install root resolved.`);
|
||||
}
|
||||
if (!isWithinRoot(target, root)) {
|
||||
throw new Error(
|
||||
`Refusing to ${action} outside the install root: '${target}' is not within '${root}'.`
|
||||
);
|
||||
}
|
||||
return realpathNearestExisting(target);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
realpathNearestExisting,
|
||||
isWithinRoot,
|
||||
assertWithinTrustedRoot,
|
||||
};
|
||||
Reference in New Issue
Block a user