fix(content-floor): quote-aware redirect in matchBashHardBlacklist (port b0cd18d7 after --ours merge)

This commit is contained in:
Дмитрий
2026-06-09 13:35:24 +03:00
parent d59b9177d4
commit 440fd71bbf
2 changed files with 36 additions and 3 deletions
+20 -3
View File
@@ -152,6 +152,16 @@ export function stderrRedirectBlock(cmd) {
return null;
}
// quote-aware redirect (порт прод-фикса b0cd18d7): `>` / `2>` внутри кавычек — литерал, не
// редирект (напр. `git commit -m "a > b"`). Стрип применяется ТОЛЬКО к redirect-проверке;
// остальной BASH_HARD_BLACKLIST идёт по СЫРОЙ строке (паттерны #4 node-inline-fs и пр. смотрят
// ВНУТРЬ кавычек — стрипать для них нельзя). Массив сохранён целым (инварианты §12 не трогаем).
export function stripQuotedSpans(s) {
return String(s || '').replace(/"[^"]*"|'[^']*'/g, ' ');
}
const STDOUT_REDIRECT_REASON = 'stdout redirect (>/>>) запрещён';
const STDOUT_REDIRECT_RE = /(?:^|[^0-9>&])>{1,2}(?![>&])/;
export const BASH_HARD_BLACKLIST = [
// v3.9 keep
{ re: /(^|\s|;|&&|\|\|)rm\b/, reason: 'rm запрещён' },
@@ -160,7 +170,7 @@ export const BASH_HARD_BLACKLIST = [
{ re: /(^|\s|;|&&|\|\|)chmod\b/, reason: 'chmod запрещён' },
{ re: /(^|\s|;|&&|\|\|)chown\b/, reason: 'chown запрещён' },
{ re: /(^|\s|;|&&|\|\|)chgrp\b/, reason: 'chgrp запрещён' },
{ re: /(?:^|[^0-9>&])>{1,2}(?![>&])/, reason: 'stdout redirect (>/>>) запрещён' },
{ re: STDOUT_REDIRECT_RE, reason: STDOUT_REDIRECT_REASON },
{ re: /\b(?:node|nodejs)\s+(?:[^|;]*\s)?(?:-e|--eval|-p|--print)\b/, reason: 'node -e/--eval/-p запрещён' },
{ re: /\bnode\s+(?:[^|;]*\s)?(?:-r|--require|--import|--experimental-loader)\b/, reason: 'node -r/--import запрещён' },
{ re: /\bpython3?\s+-c\b/, reason: 'python -c запрещён' },
@@ -185,9 +195,16 @@ export const BASH_HARD_BLACKLIST = [
export function matchBashHardBlacklist(command) {
const s = String(command || '');
if (hasInjection(s)) return '#34: echo/printf prompt-injection запрещён';
const stderr = stderrRedirectBlock(s);
const stripped = stripQuotedSpans(s);
const stderr = stderrRedirectBlock(stripped);
if (stderr) return stderr;
return matchAny(BASH_HARD_BLACKLIST, s);
for (const { re, reason } of BASH_HARD_BLACKLIST) {
if (!re.test(s)) continue;
// redirect-паттерн судим по строке без кавычек: `>` внутри кавычек — не редирект
if (reason === STDOUT_REDIRECT_REASON && !STDOUT_REDIRECT_RE.test(stripped)) continue;
return reason;
}
return null;
}
// ── PowerShell hard-blacklist — единый дом (M7 PS single-source, зеркало BASH_HARD_BLACKLIST/P-1) ──
+16
View File
@@ -318,6 +318,22 @@ describe('matchBashHardBlacklist hosted in shell-content-rules (M7 P-1)', () =>
});
});
// quote-aware redirect (порт прод-фикса b0cd18d7 после --ours merge): `>` внутри кавычек —
// не настоящий редирект (напр. commit message с `>`), не ложно-блокируется; реальный
// редирект вне кавычек по-прежнему рубится. Стрип кавычек применяется ТОЛЬКО к redirect-
// проверке (паттерны вроде #4 node-inline-fs смотрят внутрь кавычек и идут по сырой строке).
describe('matchBashHardBlacklist — quote-aware redirect (порт b0cd18d7)', () => {
it('> внутри кавычек НЕ редирект (commit message не ложно-блокируется)', () => {
expect(matchBashHardBlacklist('git commit -m "fix: a > b"')).toBe(null);
});
it('реальный stdout redirect вне кавычек по-прежнему блокируется', () => {
expect(matchBashHardBlacklist('echo secret > /tmp/x')).toBe('stdout redirect (>/>>) запрещён');
});
it('реальный stderr redirect к файлу по-прежнему блокируется', () => {
expect(matchBashHardBlacklist('git status 2> /tmp/err')).toBeTruthy();
});
});
import { matchPsHardBlacklist, PS_HARD_BLACKLIST } from './shell-content-rules.mjs';
// M7 PS single-source (variant-analysis закрыл дрейф): PS_HARD_BLACKLIST переехал в единый дом