From 440fd71bbf7b3e2df7042dc118b23a295ed532ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 9 Jun 2026 13:35:24 +0300 Subject: [PATCH] fix(content-floor): quote-aware redirect in matchBashHardBlacklist (port b0cd18d7 after --ours merge) --- tools/shell-content-rules.mjs | 23 ++++++++++++++++++++--- tools/shell-content-rules.test.mjs | 16 ++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/tools/shell-content-rules.mjs b/tools/shell-content-rules.mjs index 492ae1da..4a8f2c58 100644 --- a/tools/shell-content-rules.mjs +++ b/tools/shell-content-rules.mjs @@ -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) ── diff --git a/tools/shell-content-rules.test.mjs b/tools/shell-content-rules.test.mjs index 0c8af94a..d9592aed 100644 --- a/tools/shell-content-rules.test.mjs +++ b/tools/shell-content-rules.test.mjs @@ -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 переехал в единый дом