fix(router-gate): allow 2>&1 fd-duplication, keep file-redirect block (review finding)

This commit is contained in:
Дмитрий
2026-05-29 20:45:23 +03:00
parent b4e96be14c
commit ee7acf6eaa
2 changed files with 32 additions and 9 deletions
+10 -9
View File
@@ -23,20 +23,21 @@ import {
import { readStdin, parseEventJson, exitDecision } from './enforce-hook-helpers.mjs';
// ── stderr redirect (C16) ──
const STDERR_REDIRECT = /(?:2>>|2>|&>>|&>|\|&|2>&1\s*>)\s*([^\s|;&]+)?/g;
const SAFE_SINKS = new Set(['/dev/null', '&1', '$null', 'nul']);
function stderrRedirectBlock(cmd) {
STDERR_REDIRECT.lastIndex = 0;
// "2>&1 >file": stderr merged into stdout, then stdout redirected to a file → block.
if (/2>&1\s*>\s*[^\s|;&]/.test(cmd)) return 'C16: stderr→stdout с последующим file-redirect';
const RE = /(2>>|2>|&>>|&>|\|&)\s*([^\s|;&]+)?/g;
let m;
while ((m = STDERR_REDIRECT.exec(cmd)) !== null) {
const target = (m[1] || '').replace(/^['"]|['"]$/g, '');
if (m[0].includes('2>&1') && !m[0].trim().endsWith('2>&1')) {
// "2>&1 >file" — file redirect detected separately; still block
return 'C16: stderr→stdout с последующим file-redirect';
}
while ((m = RE.exec(cmd)) !== null) {
const op = m[1];
const after = cmd.slice(m.index + op.length);
if (/^\s*&\d/.test(after)) continue; // fd-duplication (2>&1, 1>&2) — no file, allow
const target = (m[2] || '').replace(/^['"]|['"]$/g, '');
if (!target) continue; // no file target captured → benign artifact
if (SAFE_SINKS.has(target)) continue;
return `C16: stderr redirect к «${target || 'file'}» запрещён`;
return `C16: stderr redirect к «${target}» запрещён`;
}
return null;
}
+22
View File
@@ -137,3 +137,25 @@ describe('resolvePathNormalize', () => {
expect(fn('"a\\b"')).toBe('a/b'); // default behaviour
});
});
describe('stderr redirect — 2>&1 fd-duplication (review fix)', () => {
it('allows cat a 2>&1 (merge to stdout, no file)', () => {
expect(classifyBashCommand('cat a 2>&1', {}).result).toBe('allow');
});
it('allows cat a 2>/dev/null', () => {
expect(classifyBashCommand('cat a 2>/dev/null', {}).result).toBe('allow');
});
it('still blocks stderr redirect to a file', () => {
expect(classifyBashCommand('cat a 2> err.log', {}).result).toBe('block');
expect(classifyBashCommand('cat a 2>> err.log', {}).result).toBe('block');
});
it('still blocks &> file', () => {
expect(classifyBashCommand('cat a &> out.log', {}).result).toBe('block');
});
it('allows 1>&2 fd-duplication', () => {
expect(classifyBashCommand('cat a 1>&2', {}).result).toBe('allow');
});
it('blocks 2>&1 followed by file redirect', () => {
expect(classifyBashCommand('cat a 2>&1 > out.txt', {}).result).toBe('block');
});
});