diff --git a/scripts/hooks/gateguard-heredoc.js b/scripts/hooks/gateguard-heredoc.js index d29b58fe9..31e41b29b 100644 --- a/scripts/hooks/gateguard-heredoc.js +++ b/scripts/hooks/gateguard-heredoc.js @@ -134,11 +134,12 @@ function hasLineContinuation(line) { function normalizeUnquotedHeredocLines(lines, stripTabs = false) { const logical = lines .map((line, index) => { - if (index === lines.length - 1) return line; - return hasLineContinuation(line) ? line.slice(0, -1) : `${line}\n`; + const normalized = stripTabs ? line.replace(/^\t+/, '') : line; + if (index === lines.length - 1) return normalized; + return hasLineContinuation(normalized) ? normalized.slice(0, -1) : `${normalized}\n`; }) .join(''); - return stripTabs ? logical.replace(/^\t+/, '') : logical; + return logical; } /** @returns {{ text: string, nextIndex: number }} */ diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index 5023dac5c..54a19c0e0 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -1783,10 +1783,10 @@ function runTests() { else failed++; if ( - test('denies split options after tab-stripped heredoc line continuation', () => { - expectDestructiveDeny( + test('allows a joined command when tab stripping removes the option separator', () => { + expectAllow( ['cat <<-EOF', '\t$(rm\\', '\t-rf /tmp/expanded-target)', 'EOF'].join('\n'), - 'split option in tab-stripped unquoted heredoc substitution' + 'tab stripping joins rm and -rf into a harmless command name' ); }) ) @@ -1794,10 +1794,10 @@ function runTests() { else failed++; if ( - test('preserves internal tabs after tab-stripped heredoc continuations', () => { - expectAllow( + test('denies split command names after tab-stripped heredoc continuations', () => { + expectDestructiveDeny( ['cat <<-EOF', '\t$(r\\', '\tm -rf /tmp/expanded-target)', 'EOF'].join('\n'), - 'internal tab after tab-stripped heredoc continuation' + 'split command name in tab-stripped unquoted heredoc substitution' ); }) )