(
command: string,
options?: { quotedOnly?: boolean },
)
| 111 | * ``` |
| 112 | */ |
| 113 | export function extractHeredocs( |
| 114 | command: string, |
| 115 | options?: { quotedOnly?: boolean }, |
| 116 | ): HeredocExtractionResult { |
| 117 | const heredocs = new Map<string, HeredocInfo>() |
| 118 | |
| 119 | // Quick check: if no << present, skip processing |
| 120 | if (!command.includes('<<')) { |
| 121 | return { processedCommand: command, heredocs } |
| 122 | } |
| 123 | |
| 124 | // Security: Paranoid pre-validation. Our incremental quote/comment scanner |
| 125 | // (see advanceScan below) does simplified parsing that cannot handle all |
| 126 | // bash quoting constructs. If the command contains |
| 127 | // constructs that could desync our quote tracking, bail out entirely |
| 128 | // rather than risk extracting a heredoc with incorrect boundaries. |
| 129 | // This is defense-in-depth: each construct below has caused or could |
| 130 | // cause a security bypass if we attempt extraction. |
| 131 | // |
| 132 | // Specifically, we bail if the command contains: |
| 133 | // 1. $'...' or $"..." (ANSI-C / locale quoting — our quote tracker |
| 134 | // doesn't handle the $ prefix, would misparse the quotes) |
| 135 | // 2. Backtick command substitution (backtick nesting has complex parsing |
| 136 | // rules, and backtick acts as shell_eof_token for PST_EOFTOKEN in |
| 137 | // make_cmd.c:606, enabling early heredoc closure that our parser |
| 138 | // can't replicate) |
| 139 | if (/\$['"]/.test(command)) { |
| 140 | return { processedCommand: command, heredocs } |
| 141 | } |
| 142 | // Check for backticks in the command text before the first <<. |
| 143 | // Backtick nesting has complex parsing rules, and backtick acts as |
| 144 | // shell_eof_token for PST_EOFTOKEN (make_cmd.c:606), enabling early |
| 145 | // heredoc closure that our parser can't replicate. We only check |
| 146 | // before << because backticks in heredoc body content are harmless. |
| 147 | const firstHeredocPos = command.indexOf('<<') |
| 148 | if (firstHeredocPos > 0 && command.slice(0, firstHeredocPos).includes('`')) { |
| 149 | return { processedCommand: command, heredocs } |
| 150 | } |
| 151 | |
| 152 | // Security: Check for arithmetic evaluation context before the first `<<`. |
| 153 | // In bash, `(( x = 1 << 2 ))` uses `<<` as a BIT-SHIFT operator, not a |
| 154 | // heredoc. If we mis-extract it, subsequent lines become "heredoc content" |
| 155 | // and are hidden from security validators, while bash executes them as |
| 156 | // separate commands. We bail entirely if `((` appears before `<<` without |
| 157 | // a matching `))` — we can't reliably distinguish arithmetic `<<` from |
| 158 | // heredoc `<<` in that context. Note: $(( is already caught by |
| 159 | // validateDangerousPatterns, but bare (( is not. |
| 160 | if (firstHeredocPos > 0) { |
| 161 | const beforeHeredoc = command.slice(0, firstHeredocPos) |
| 162 | // Count (( and )) occurrences — if unbalanced, `<<` may be arithmetic |
| 163 | const openArith = (beforeHeredoc.match(/\(\(/g) || []).length |
| 164 | const closeArith = (beforeHeredoc.match(/\)\)/g) || []).length |
| 165 | if (openArith > closeArith) { |
| 166 | return { processedCommand: command, heredocs } |
| 167 | } |
| 168 | } |
| 169 | |
| 170 | // Create a global version of the pattern for iteration |
no test coverage detected