MCPcopy Index your code
hub / github.com/codeaashu/claude-code / extractOutputRedirections

Function extractOutputRedirections

src/utils/bash/commands.ts:634–790  ·  view source on GitHub ↗
(cmd: string)

Source from the content-addressed store, hash-verified

632 * @returns Object containing the command without redirections and the target paths if found
633 */
634export function extractOutputRedirections(cmd: string): {
635 commandWithoutRedirections: string
636 redirections: Array<{ target: string; operator: '>' | '>>' }>
637 hasDangerousRedirection: boolean
638} {
639 const redirections: Array<{ target: string; operator: '>' | '>>' }> = []
640 let hasDangerousRedirection = false
641
642 // SECURITY: Extract heredocs BEFORE line-continuation joining AND parsing.
643 // This matches splitCommandWithOperators (line 101). Quoted-heredoc bodies
644 // are LITERAL text in bash (`<< 'EOF'\n${}\nEOF` — ${} is NOT expanded, and
645 // `\<newline>` is NOT a continuation). But shell-quote doesn't understand
646 // heredocs; it sees `${}` on line 2 as an unquoted bad substitution and throws.
647 //
648 // ORDER MATTERS: If we join continuations first, a quoted heredoc body
649 // containing `x\<newline>DELIM` gets joined to `xDELIM` — the delimiter
650 // shifts, and `> /etc/passwd` that bash executes gets swallowed into the
651 // heredoc body and NEVER reaches path validation.
652 //
653 // Attack: `cat <<'ls'\nx\\\nls\n> /etc/passwd\nls` with Bash(cat:*)
654 // - bash: quoted heredoc → `\` is literal, body = `x\`, next `ls` closes
655 // heredoc → `> /etc/passwd` TRUNCATES the file, final `ls` runs
656 // - join-first (OLD, WRONG): `x\<NL>ls` → `xls`, delimiter search finds
657 // the LAST `ls`, body = `xls\n> /etc/passwd` → redirections:[] →
658 // /etc/passwd NEVER validated → FILE WRITE, no prompt
659 // - extract-first (NEW, matches splitCommandWithOperators): body = `x\`,
660 // `> /etc/passwd` survives → captured → path-validated
661 //
662 // Original attack (why extract-before-parse exists at all):
663 // `echo payload << 'EOF' > /etc/passwd\n${}\nEOF` with Bash(echo:*)
664 // - bash: quoted heredoc → ${} literal, echo writes "payload\n" to /etc/passwd
665 // - checkPathConstraints: calls THIS function on original → ${} crashes
666 // shell-quote → previously returned {redirections:[], dangerous:false}
667 // → /etc/passwd NEVER validated → FILE WRITE, no prompt.
668 const { processedCommand: heredocExtracted, heredocs } = extractHeredocs(cmd)
669
670 // SECURITY: Join line continuations AFTER heredoc extraction, BEFORE parsing.
671 // Without this, `> \<newline>/etc/passwd` causes shell-quote to emit an
672 // empty-string token for `\<newline>` and a separate token for the real path.
673 // The extractor picks up `''` as the target; isSimpleTarget('') was vacuously
674 // true (now also fixed as defense-in-depth); path.resolve(cwd,'') returns cwd
675 // (always allowed). Meanwhile bash joins the continuation and writes to
676 // /etc/passwd. Even backslash count = newline is a separator (not continuation).
677 const processedCommand = heredocExtracted.replace(/\\+\n/g, match => {
678 const backslashCount = match.length - 1
679 if (backslashCount % 2 === 1) {
680 return '\\'.repeat(backslashCount - 1)
681 }
682 return match
683 })
684
685 // Try to parse the heredoc-extracted command
686 const parseResult = tryParseShellCommand(processedCommand, env => `$${env}`)
687
688 // SECURITY: FAIL-CLOSED on parse failure. Previously returned
689 // {redirections:[], hasDangerousRedirection:false} — a silent bypass.
690 // If shell-quote can't parse (even after heredoc extraction), we cannot
691 // verify what redirections exist. Any `>` in the command could write files.

Calls 11

extractHeredocsFunction · 0.85
tryParseShellCommandFunction · 0.85
handleRedirectionFunction · 0.85
restoreHeredocsFunction · 0.85
reconstructCommandFunction · 0.85
forEachMethod · 0.80
popMethod · 0.80
isOperatorFunction · 0.70
pushMethod · 0.45
addMethod · 0.45
hasMethod · 0.45

Tested by

no test coverage detected