mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-10 03:37:54 +02:00
fix: pin Hookify rules before path inspection
Open rule files before trusting path metadata, compare the descriptor identity to the live path, and only then validate containment and read bytes. Replace the unsafe temp-file race simulation with an open-first descriptor/path-swap regression.
This commit is contained in:
@@ -355,6 +355,31 @@ function loadRuleFile({
|
||||
const filePath = path.join(claudeDir, fileName);
|
||||
let fileDescriptor;
|
||||
try {
|
||||
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
||||
try {
|
||||
fileDescriptor = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
|
||||
} catch (error) {
|
||||
if (
|
||||
error &&
|
||||
['ELOOP', 'EMLINK', 'ENOENT', 'ENOTDIR'].includes(error.code)
|
||||
) {
|
||||
return {
|
||||
rule: null,
|
||||
diagnostic: diagnostic('HOOKIFY_RULE_FILE_UNSAFE', fileName, 'not a regular file'),
|
||||
bytesRead: 0,
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const fileStat = fs.fstatSync(fileDescriptor);
|
||||
if (!fileStat.isFile()) {
|
||||
return {
|
||||
rule: null,
|
||||
diagnostic: diagnostic('HOOKIFY_RULE_FILE_UNSAFE', fileName, 'not a regular file'),
|
||||
bytesRead: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const linkStat = fs.lstatSync(filePath);
|
||||
if (linkStat.isSymbolicLink() || !linkStat.isFile()) {
|
||||
return {
|
||||
@@ -363,6 +388,23 @@ function loadRuleFile({
|
||||
bytesRead: 0,
|
||||
};
|
||||
}
|
||||
if (
|
||||
fileStat.dev !== linkStat.dev ||
|
||||
fileStat.ino !== linkStat.ino ||
|
||||
fileStat.mode !== linkStat.mode ||
|
||||
fileStat.size !== linkStat.size ||
|
||||
fileStat.mtimeMs !== linkStat.mtimeMs
|
||||
) {
|
||||
return {
|
||||
rule: null,
|
||||
diagnostic: diagnostic(
|
||||
'HOOKIFY_RULE_FILE_UNSAFE',
|
||||
fileName,
|
||||
'rule identity changed during evaluation'
|
||||
),
|
||||
bytesRead: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const realDirectory = fs.realpathSync(claudeDir);
|
||||
if (expectedRealDirectory && realDirectory !== expectedRealDirectory) {
|
||||
@@ -380,55 +422,17 @@ function loadRuleFile({
|
||||
if (
|
||||
path.dirname(realFile) !== realDirectory ||
|
||||
realFile !== path.join(realDirectory, fileName)
|
||||
) {
|
||||
return {
|
||||
rule: null,
|
||||
diagnostic: diagnostic('HOOKIFY_RULE_FILE_UNSAFE', fileName, 'resolved outside project .claude'),
|
||||
bytesRead: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
||||
fileDescriptor = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
|
||||
const fileStat = fs.fstatSync(fileDescriptor);
|
||||
if (
|
||||
fileStat.dev !== linkStat.dev ||
|
||||
fileStat.ino !== linkStat.ino ||
|
||||
fileStat.mode !== linkStat.mode ||
|
||||
fileStat.size !== linkStat.size ||
|
||||
fileStat.mtimeMs !== linkStat.mtimeMs
|
||||
) {
|
||||
return {
|
||||
rule: null,
|
||||
diagnostic: diagnostic(
|
||||
'HOOKIFY_RULE_FILE_UNSAFE',
|
||||
fileName,
|
||||
'rule identity changed during evaluation'
|
||||
'resolved outside project .claude'
|
||||
),
|
||||
bytesRead: 0,
|
||||
};
|
||||
}
|
||||
if (
|
||||
fs.realpathSync(claudeDir) !== realDirectory ||
|
||||
fs.realpathSync(filePath) !== realFile
|
||||
) {
|
||||
return {
|
||||
rule: null,
|
||||
diagnostic: diagnostic(
|
||||
'HOOKIFY_RULE_FILE_UNSAFE',
|
||||
fileName,
|
||||
'rule path changed during evaluation'
|
||||
),
|
||||
bytesRead: 0,
|
||||
};
|
||||
}
|
||||
if (!fileStat.isFile()) {
|
||||
return {
|
||||
rule: null,
|
||||
diagnostic: diagnostic('HOOKIFY_RULE_FILE_UNSAFE', fileName, 'not a regular file'),
|
||||
bytesRead: 0,
|
||||
};
|
||||
}
|
||||
if (fileStat.size > LIMITS.maxFileBytes) {
|
||||
return {
|
||||
rule: null,
|
||||
|
||||
Reference in New Issue
Block a user