mirror of
https://github.com/affaan-m/ECC.git
synced 2026-08-17 21:15:40 +02:00
fix(security): reject Windows reparse digests
This commit is contained in:
@@ -96,13 +96,22 @@ function readInstalledFileNoFollow(plan, operation) {
|
||||
}
|
||||
|
||||
try {
|
||||
const openedStat = fs.fstatSync(descriptor, { bigint: true });
|
||||
const finalPathStat = fs.lstatSync(operation.destinationPath, { bigint: true });
|
||||
if (finalPathStat.isSymbolicLink() || !finalPathStat.isFile()) {
|
||||
return null;
|
||||
}
|
||||
const identityMatches = openedStat.ino === finalPathStat.ino
|
||||
&& (!openedStat.dev || !finalPathStat.dev || openedStat.dev === finalPathStat.dev);
|
||||
if (!openedStat.isFile() || !identityMatches) {
|
||||
throw new Error(
|
||||
`Refusing to hash changed install destination: ${operation.destinationPath}`
|
||||
);
|
||||
}
|
||||
// Revalidate the full path after opening. The descriptor pins the file so
|
||||
// the digest and metadata refer to the same object.
|
||||
assertSafeInstallOperation(plan, operation);
|
||||
assertSafeClaudeSkillOperation(plan, operation);
|
||||
if (!fs.fstatSync(descriptor).isFile()) {
|
||||
return null;
|
||||
}
|
||||
return fs.readFileSync(descriptor);
|
||||
} finally {
|
||||
fs.closeSync(descriptor);
|
||||
@@ -457,7 +466,10 @@ function applyInstallPlan(plan, dependencies = {}) {
|
||||
stateWithContentDigests(migration.bridgeState, appliedPlan)
|
||||
);
|
||||
} catch (checkpointError) {
|
||||
error.message += ` Install-state checkpoint also failed: ${checkpointError.message}`;
|
||||
throw new Error(
|
||||
`${error.message} Install-state checkpoint also failed: ${checkpointError.message}`,
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -188,7 +188,16 @@ function managedOperation(kind, destinationPath, overrides = {}) {
|
||||
destinationPath,
|
||||
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0)
|
||||
);
|
||||
if (fs.fstatSync(descriptor).isFile()) {
|
||||
const openedStat = fs.fstatSync(descriptor, { bigint: true });
|
||||
const finalPathStat = fs.lstatSync(destinationPath, { bigint: true });
|
||||
const identityMatches = openedStat.ino === finalPathStat.ino
|
||||
&& (!openedStat.dev || !finalPathStat.dev || openedStat.dev === finalPathStat.dev);
|
||||
if (
|
||||
openedStat.isFile()
|
||||
&& finalPathStat.isFile()
|
||||
&& !finalPathStat.isSymbolicLink()
|
||||
&& identityMatches
|
||||
) {
|
||||
operation.contentSha256 = crypto.createHash('sha256')
|
||||
.update(fs.readFileSync(descriptor))
|
||||
.digest('hex');
|
||||
@@ -212,6 +221,25 @@ function runTests() {
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
if (test('managed-operation digest never follows a final symlink', () => {
|
||||
const tempDir = createTempDir('install-lifecycle-symlink-digest-');
|
||||
const victimPath = path.join(tempDir, 'victim.md');
|
||||
const symlinkPath = path.join(tempDir, 'managed.md');
|
||||
try {
|
||||
fs.writeFileSync(victimPath, 'user content\n');
|
||||
try {
|
||||
fs.symlinkSync(victimPath, symlinkPath, 'file');
|
||||
} catch {
|
||||
console.log(' (file symlink unsupported on this platform; skipping)');
|
||||
return;
|
||||
}
|
||||
const operation = managedOperation('copy-file', symlinkPath);
|
||||
assert.strictEqual(operation.contentSha256, undefined);
|
||||
} finally {
|
||||
cleanup(tempDir);
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('normalizes default targets and dedupes adapter aliases', () => {
|
||||
const defaultTargets = normalizeTargets();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user