fix(cursor): validate blocking hook input

This commit is contained in:
wellkilo committed 2026-09-07 09:41:16 +08:00
1 parent 0718ec240f
commit 2de2a592dc
5 files changed
+57 -18

No files matched your search

+8 -7
View File
@@ -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 }
: '');
});
});
+2 -1
View File
@@ -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');
+8 -6
View File
@@ -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;
}
+7 -4
View File
@@ -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}`);