mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-07 16:45:53 +00:00
fix(cursor): validate blocking hook input
This commit is contained in:
1 parent
0718ec240f
commit
2de2a592dc
5 files changed
+57
-18
No files matched your search
@@ -15,11 +15,12 @@ const STOP_FORMAT_TYPECHECK_TIMEOUT_HEADROOM_MS = 15000;
|
||||
|
||||
function readStdin(options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const stream = options.stream || process.stdin;
|
||||
let data = '';
|
||||
let dataBytes = 0;
|
||||
let truncated = false;
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => {
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', chunk => {
|
||||
const chunkBytes = Buffer.byteLength(chunk, 'utf8');
|
||||
if (truncated || dataBytes + chunkBytes > MAX_STDIN_BYTES) {
|
||||
truncated = true;
|
||||
@@ -28,7 +29,7 @@ function readStdin(options = {}) {
|
||||
data += chunk;
|
||||
dataBytes += chunkBytes;
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
stream.on('end', () => {
|
||||
if (truncated) {
|
||||
if (options.includeMetadata !== true) {
|
||||
process.stderr.write(
|
||||
@@ -36,18 +37,18 @@ function readStdin(options = {}) {
|
||||
);
|
||||
}
|
||||
resolve(options.includeMetadata === true
|
||||
? { raw: '', truncated: true }
|
||||
? { raw: '', truncated: true, readError: false }
|
||||
: '');
|
||||
return;
|
||||
}
|
||||
resolve(options.includeMetadata === true
|
||||
? { raw: data, truncated: false }
|
||||
? { raw: data, truncated: false, readError: false }
|
||||
: data);
|
||||
});
|
||||
process.stdin.on('error', error => {
|
||||
stream.on('error', error => {
|
||||
process.stderr.write(`[Cursor Hook] stdin read failed: ${error.message}\n`);
|
||||
resolve(options.includeMetadata === true
|
||||
? { raw: '', truncated: false }
|
||||
? { raw: '', truncated: false, readError: true }
|
||||
: '');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,8 @@ readStdin().then(raw => {
|
||||
}
|
||||
try {
|
||||
const input = JSON.parse(raw);
|
||||
const filePath = input.file_path || input.path || input.file || '';
|
||||
const filePath = [input.file_path, input.path, input.file]
|
||||
.find(value => typeof value === 'string' && value.trim()) || '';
|
||||
if (/\.(env|key|pem)$|\.env\.|credentials|secret/i.test(filePath)) {
|
||||
console.error('[ECC] WARNING: Reading sensitive file: ' + filePath);
|
||||
console.error('[ECC] Ensure this data is not exposed in outputs');
|
||||
|
||||
@@ -7,8 +7,8 @@ const {
|
||||
} = require('./adapter');
|
||||
|
||||
readStdin({ includeMetadata: true })
|
||||
.then(({ raw, truncated }) => {
|
||||
if (truncated) {
|
||||
.then(({ raw, truncated, readError }) => {
|
||||
if (truncated || readError) {
|
||||
const hasBlockingGuard = (
|
||||
hookEnabled('pre:bash:block-no-verify', ['minimal', 'standard', 'strict'])
|
||||
|| (
|
||||
@@ -17,12 +17,14 @@ readStdin({ includeMetadata: true })
|
||||
)
|
||||
);
|
||||
if (hasBlockingGuard) {
|
||||
console.error(
|
||||
'[Cursor Hook] stdin exceeded the safety limit; blocking beforeShellExecution'
|
||||
);
|
||||
console.error(truncated
|
||||
? '[Cursor Hook] stdin exceeded the safety limit; blocking beforeShellExecution'
|
||||
: '[Cursor Hook] stdin read failed; blocking beforeShellExecution');
|
||||
process.exit(2);
|
||||
}
|
||||
console.error('[Cursor Hook] stdin exceeded the safety limit; suppressing truncated input');
|
||||
console.error(truncated
|
||||
? '[Cursor Hook] stdin exceeded the safety limit; suppressing truncated input'
|
||||
: '[Cursor Hook] stdin read failed; suppressing untrusted input');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
const { readStdin, hookEnabled } = require('./adapter');
|
||||
readStdin({ includeMetadata: true }).then(({ raw, truncated }) => {
|
||||
readStdin({ includeMetadata: true }).then(({ raw, truncated, readError }) => {
|
||||
if (!hookEnabled('pre:tab-read:sensitive-file-block', ['minimal', 'standard', 'strict'])) {
|
||||
process.stdout.write(raw);
|
||||
return;
|
||||
}
|
||||
if (truncated) {
|
||||
console.error('[Cursor Hook] stdin exceeded the safety limit; blocking beforeTabFileRead');
|
||||
if (truncated || readError) {
|
||||
console.error(truncated
|
||||
? '[Cursor Hook] stdin exceeded the safety limit; blocking beforeTabFileRead'
|
||||
: '[Cursor Hook] stdin read failed; blocking beforeTabFileRead');
|
||||
process.exit(2);
|
||||
}
|
||||
try {
|
||||
const input = JSON.parse(raw);
|
||||
const filePath = input.file_path || input.path || input.file || '';
|
||||
const filePath = [input.file_path, input.path, input.file]
|
||||
.find(value => typeof value === 'string' && value.trim()) || '';
|
||||
if (/\.(env|key|pem)$|\.env\.|credentials|secret/i.test(filePath)) {
|
||||
console.error('[ECC] BLOCKED: Tab cannot read sensitive file: ' + filePath);
|
||||
process.exit(2);
|
||||
|
||||
@@ -287,6 +287,17 @@ if (test('Cursor-sensitive file hooks recognize the official file_path field', (
|
||||
assert.match(blocked.stderr, /BLOCKED/);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('invalid path field types cannot shadow a valid string fallback', () => {
|
||||
const input = { file_path: {}, path: '.env' };
|
||||
const warning = runHook(BEFORE_READ, input);
|
||||
assert.strictEqual(warning.status, 0, warning.stderr);
|
||||
assert.match(warning.stderr, /Reading sensitive file/);
|
||||
|
||||
const blocked = runHook(BEFORE_TAB_READ, input);
|
||||
assert.strictEqual(blocked.status, 2, blocked.stderr);
|
||||
assert.match(blocked.stderr, /BLOCKED/);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('Cursor-native audit hooks stay disabled in the minimal profile', () => {
|
||||
const cases = [
|
||||
[BEFORE_MCP, { server: 'example', tool: 'read' }],
|
||||
@@ -661,6 +672,27 @@ if (test('oversized beforeTabFileRead input fails closed', () => {
|
||||
assert.match(result.stderr, /stdin exceeded.*blocking/i);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('readStdin exposes stream failures to blocking dispatchers', () => {
|
||||
const probe = [
|
||||
"const { EventEmitter } = require('events');",
|
||||
`const { readStdin } = require(${JSON.stringify(path.join(REPO_ROOT, '.cursor', 'hooks', 'adapter.js'))});`,
|
||||
"const stream = new EventEmitter();",
|
||||
"stream.setEncoding = () => {};",
|
||||
"readStdin({ includeMetadata: true, stream }).then(result => process.stdout.write(JSON.stringify(result)));",
|
||||
"setImmediate(() => stream.emit('error', new Error('synthetic read failure')));",
|
||||
].join('\n');
|
||||
const result = spawnSync(process.execPath, ['-e', probe], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5000,
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr);
|
||||
assert.deepStrictEqual(JSON.parse(result.stdout), {
|
||||
raw: '',
|
||||
truncated: false,
|
||||
readError: true,
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log('-'.repeat(55));
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
|
||||
Reference in new issue
Block a user