@@ -289,6 +289,97 @@ test("classifyBash: allows redirect into unrelated dotfiles", () => {
289289 assert . equal ( classifyBash ( "echo foo > ~/.config/app.conf" ) , null ) ;
290290} ) ;
291291
292+ // ---------------------------------------------------------------------------
293+ // Review round-3 bypass regressions — quoted dangerous targets
294+ //
295+ // Round-2 classifier rewrite used a shared `DANGEROUS_TARGET` matcher that
296+ // required an UNquoted path right after the rm/chmod/chown command token.
297+ // Reviewer probe caught six real shell spellings that bypassed the rule by
298+ // wrapping the target in "..." or '...'. Round-3 adds `['"]?` before the
299+ // target in rm / chmod+chown / redirect-to-system-path rules. These tests
300+ // nail down the exact bypass strings so the gap never reopens.
301+ // ---------------------------------------------------------------------------
302+
303+ test ( "classifyBash: blocks rm with double-quoted dangerous target" , ( ) => {
304+ const cases = [
305+ `rm "/etc/hosts"` ,
306+ `rm "/etc/passwd"` ,
307+ `rm --recursive --force "/"` ,
308+ `rm -rf "/System/Library"` ,
309+ `rm -rf "/usr/local/bin/foo"` ,
310+ `rm -rf "$HOME"` ,
311+ `rm "$HOME/"` ,
312+ ] ;
313+ for ( const cmd of cases ) {
314+ const d = classifyBash ( cmd ) ;
315+ assert . ok ( d , `expected deny: ${ cmd } ` ) ;
316+ assert . match ( d ! . reason , / r o o t o r s y s t e m p a t h / ) ;
317+ }
318+ } ) ;
319+
320+ test ( "classifyBash: blocks rm with single-quoted dangerous target" , ( ) => {
321+ const cases = [
322+ `rm '/etc/hosts'` ,
323+ `rm -rf '/System/Library'` ,
324+ `rm --force --recursive '/usr'` ,
325+ ] ;
326+ for ( const cmd of cases ) {
327+ const d = classifyBash ( cmd ) ;
328+ assert . ok ( d , `expected deny: ${ cmd } ` ) ;
329+ assert . match ( d ! . reason , / r o o t o r s y s t e m p a t h / ) ;
330+ }
331+ } ) ;
332+
333+ test ( "classifyBash: blocks chmod/chown with quoted dangerous target" , ( ) => {
334+ const cases = [
335+ `chmod 000 "/"` ,
336+ `chmod 000 '/'` ,
337+ `chmod -R 000 "$HOME"` ,
338+ `chmod -R 000 "/etc"` ,
339+ `chown root:wheel "/usr"` ,
340+ `chown -R root:wheel '/System/Library'` ,
341+ ] ;
342+ for ( const cmd of cases ) {
343+ const d = classifyBash ( cmd ) ;
344+ assert . ok ( d , `expected deny: ${ cmd } ` ) ;
345+ assert . match ( d ! . reason , / p e r m i s s i o n s o r o w n e r s h i p o f a r o o t o r s y s t e m / ) ;
346+ }
347+ } ) ;
348+
349+ test ( "classifyBash: blocks redirect into quoted system paths" , ( ) => {
350+ const cases = [
351+ `echo bad > "/etc/hosts"` ,
352+ `echo bad > '/etc/hosts'` ,
353+ `cat bad.txt >> "/etc/passwd"` ,
354+ `echo x > "/System/thing"` ,
355+ `echo x > "/usr/bin/foo"` ,
356+ `echo x > "/dev/disk2"` ,
357+ ] ;
358+ for ( const cmd of cases ) {
359+ const d = classifyBash ( cmd ) ;
360+ assert . ok ( d , `expected deny: ${ cmd } ` ) ;
361+ assert . match ( d ! . reason , / s y s t e m p a t h / ) ;
362+ }
363+ } ) ;
364+
365+ test ( "classifyBash: still allows quoted non-system targets" , ( ) => {
366+ // Regression: the round-3 quoted-leading-char expansion must not bleed into
367+ // scratch paths. `rm "/tmp/scratch"` is a normal dev operation.
368+ const allowed = [
369+ `rm "/tmp/scratch"` ,
370+ `rm -rf "/tmp/scratch"` ,
371+ `rm -rf "./build"` ,
372+ `rm -rf "node_modules"` ,
373+ `chmod 644 "./src/index.ts"` ,
374+ `chown staff "/tmp/mine"` ,
375+ `echo hi > "/usr/local/etc/foo.conf"` ,
376+ `echo hi > "/Library/Caches/com.omi.tmp"` ,
377+ ] ;
378+ for ( const cmd of allowed ) {
379+ assert . equal ( classifyBash ( cmd ) , null , `expected allow: ${ cmd } ` ) ;
380+ }
381+ } ) ;
382+
292383// ---------------------------------------------------------------------------
293384// classifyFileWrite
294385// ---------------------------------------------------------------------------
0 commit comments