fix(router-gate): allow 2>&1 fd-duplication, keep file-redirect block (review finding)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user