mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-01 22:15:17 +02:00
fix(gateguard): sanitize dangerous invisible unicode in denial paths (#3103)
* fix(gateguard): sanitize dangerous invisible unicode in denial paths sanitizePath only stripped control characters and bidi overrides, so the 394 codepoints that the repo-wide unicode safety policy classifies as dangerous-invisible (zero-width U+200B-200D, U+2060, U+2061-2064, variation selectors U+FE00-FE0F, tag block U+E0000-E007F, Hangul fillers, U+FEFF, U+180E, U+3164) plus the U+2028/U+2029 line and paragraph separators passed verbatim into Edit/Write denial messages. Invisible characters in a quoted file path let a malicious path look clean to a human reviewer. Align sanitizePath with the CI policy list and cover it with a hook-output regression test that asserts no dangerous invisible character survives into the denial reason while visible path text stays intact. * refactor(gateguard): name the sanitizePath unicode policy constants Address review feedback: move the code points and ranges out of sanitizePath into descriptive constants mirroring scripts/ci/check-unicode-safety.js, so the policy is auditable in one place. Also seed the regression test input with U+2029 so the paragraph-separator assertion is exercised rather than vacuous. * fix(gateguard): sanitize C1 control characters in denial paths Follow-up on the sanitizePath policy alignment: the Unicode C1 control block (U+0080..U+009F) is invisible in every renderer like the ASCII controls below 0x20, but the strip only covered the ASCII range. Add C1_CONTROLS to the policy constants and seed U+0091 into the regression test input.
This commit is contained in:
@@ -3369,6 +3369,34 @@ function runTests() {
|
||||
console.error(` [cleanup] failed to remove ${stateDir}: ${err.message}`);
|
||||
}
|
||||
|
||||
// --- sanitizePath dangerous invisible unicode regression ---
|
||||
clearState();
|
||||
if (
|
||||
test('sanitizePath strips CI-defined dangerous invisible unicode from denial paths', () => {
|
||||
const file_path =
|
||||
'/src/eu2028\u2028eu2029\u2029app.js\u200bhidden\u2060name\ufefftail\u3164x\u0091c1.js';
|
||||
const input = {
|
||||
tool_name: 'Edit',
|
||||
tool_input: { file_path, old_string: 'foo', new_string: 'bar' }
|
||||
};
|
||||
const result = runHook(input);
|
||||
const output = parseOutput(result.stdout);
|
||||
const reason = String(
|
||||
output && output.hookSpecificOutput
|
||||
? output.hookSpecificOutput.permissionDecisionReason
|
||||
: ''
|
||||
);
|
||||
for (const bad of ['\u2028', '\u2029', '\u200b', '\u2060', '\ufeff', '\u3164', '\u0091']) {
|
||||
assert.ok(!reason.includes(bad), `denial reason must not carry U+${bad.codePointAt(0).toString(16)} (${bad})`);
|
||||
}
|
||||
assert.ok(reason.includes('app.js'), 'visible path text must remain');
|
||||
})
|
||||
) {
|
||||
passed++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
|
||||
console.log(`\n ${passed} passed, ${failed} failed\n`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user