@@ -53,10 +53,11 @@ const BLOCKED_PATTERNS: RegExp[] = [
5353 // chmod/chown against system binaries and config trees.
5454 / \b c h m o d \s + .* \/ ( e t c | s y s | p r o c | d e v | b i n | s b i n | u s r \/ b i n | u s r \/ s b i n ) / ,
5555 / \b c h o w n \s + .* \/ ( e t c | s y s | p r o c | d e v | b i n | s b i n | u s r \/ b i n | u s r \/ s b i n ) / ,
56- // Fork bombs and busy-loops.
56+ // Fork bombs and busy-loops. These inspect quoted interpreter payloads
57+ // (`bash -c 'while :; do'`, `perl -e 'fork while fork'`), so they run on
58+ // the original subject — command-position matchers neutralize separators
59+ // inside quotes and would otherwise miss the `;` these patterns need.
5760 / : \( \) \s * \{ \s * : \| : \& \s * \} ; / ,
58- / b a s h \s + - c \s + .* w h i l e \s + : \s * ; \s * d o / ,
59- / p e r l \s + - e \s + .* f o r k \s + w h i l e \s + f o r k / ,
6061 // Piping a network download straight into a shell (through any wrappers).
6162 new RegExp ( String . raw `(curl|wget|fetch)\b[^\n;|]*\|\s*${ SHELL_WRAPPERS } (bash|sh|zsh)\b` ) ,
6263 // Privilege escalation and shell replacement, only in command position.
@@ -73,6 +74,11 @@ const BLOCKED_PATTERNS: RegExp[] = [
7374 / (?: ^ | [ \n ; & | ( ] ) \s * i n i t \s + [ 0 6 ] \b / ,
7475] ;
7576
77+ const BLOCKED_QUOTED_PAYLOAD_PATTERNS : RegExp [ ] = [
78+ / b a s h \s + - c \s + .* w h i l e \s + : \s * ; \s * d o / ,
79+ / p e r l \s + - e \s + .* f o r k \s + w h i l e \s + f o r k / ,
80+ ] ;
81+
7682// Open-ended tree walks via the shell OOM the host: `find | tail` still forces
7783// the full stream through the collector, and recursive grep/rg walks huge trees
7884// before any pipe limit applies. Hard-deny those shapes for host safety; the
@@ -871,13 +877,90 @@ function isCatastrophicRm(segment: string): boolean {
871877 return targets . length === 0 || targets . some ( isDangerousTarget ) ;
872878}
873879
880+ // Blank quoted interiors so CMD does not treat `;` inside `-m` text as a new command.
881+ function skipQuotedSpans ( command : string ) : string {
882+ let out = "" ;
883+ let quote : '"' | "'" | undefined ;
884+ let substDepth = 0 ;
885+ let inBacktick = false ;
886+ for ( let i = 0 ; i < command . length ; i ++ ) {
887+ const ch = command [ i ] ! ;
888+ if ( quote === "'" ) {
889+ if ( ch === "'" ) {
890+ quote = undefined ;
891+ out += ch ;
892+ } else {
893+ out += ch === "\n" ? "\n" : " " ;
894+ }
895+ continue ;
896+ }
897+ if ( quote === '"' ) {
898+ if ( ch === "\\" ) {
899+ const next = command [ i + 1 ] ;
900+ if ( next !== undefined && next !== "\n" ) {
901+ out += " " ;
902+ i ++ ;
903+ continue ;
904+ }
905+ }
906+ if ( ch === "`" ) {
907+ inBacktick = true ;
908+ quote = undefined ;
909+ out += ch ;
910+ continue ;
911+ }
912+ if ( ch === "$" && command [ i + 1 ] === "(" ) {
913+ substDepth ++ ;
914+ quote = undefined ;
915+ out += "$(" ;
916+ i ++ ;
917+ continue ;
918+ }
919+ if ( ch === '"' ) {
920+ quote = undefined ;
921+ out += ch ;
922+ } else {
923+ out += ch === "\n" ? "\n" : " " ;
924+ }
925+ continue ;
926+ }
927+ if ( inBacktick && ch === "`" ) {
928+ inBacktick = false ;
929+ quote = '"' ;
930+ out += ch ;
931+ continue ;
932+ }
933+ if ( substDepth > 0 && ch === "$" && command [ i + 1 ] === "(" ) {
934+ substDepth ++ ;
935+ out += "$(" ;
936+ i ++ ;
937+ continue ;
938+ }
939+ if ( substDepth > 0 && ch === "(" ) {
940+ substDepth ++ ;
941+ out += ch ;
942+ continue ;
943+ }
944+ if ( substDepth > 0 && ch === ")" ) {
945+ substDepth -- ;
946+ out += ch ;
947+ if ( substDepth === 0 ) quote = '"' ;
948+ continue ;
949+ }
950+ if ( ch === '"' || ch === "'" ) quote = ch ;
951+ out += ch ;
952+ }
953+ return out ;
954+ }
955+
874956// Scan expanded subjects for blocked patterns / catastrophic rm. Callers may
875957// pass a pre-normalized form so path-qualified binaries (`/usr/bin/sudo`) still
876958// match command-position patterns.
877959function isDestructiveExpanded ( command : string ) : boolean {
878960 const { subjects } = expandShellSubjects ( command ) ;
879961 return subjects . some ( ( subject ) => {
880- if ( BLOCKED_PATTERNS . some ( ( pattern ) => pattern . test ( subject ) ) ) return true ;
962+ if ( BLOCKED_PATTERNS . some ( ( pattern ) => pattern . test ( skipQuotedSpans ( subject ) ) ) ) return true ;
963+ if ( BLOCKED_QUOTED_PAYLOAD_PATTERNS . some ( ( pattern ) => pattern . test ( subject ) ) ) return true ;
881964 return subject . split ( CHAIN ) . some ( isCatastrophicRm ) ;
882965 } ) ;
883966}
0 commit comments