(command: string)
| 188 | * because shell-quote thinks it's all one single-quoted string. |
| 189 | */ |
| 190 | export function hasShellQuoteSingleQuoteBug(command: string): boolean { |
| 191 | // Walk the command with correct bash single-quote semantics |
| 192 | let inSingleQuote = false |
| 193 | let inDoubleQuote = false |
| 194 | |
| 195 | for (let i = 0; i < command.length; i++) { |
| 196 | const char = command[i] |
| 197 | |
| 198 | // Handle backslash escaping outside of single quotes |
| 199 | if (char === '\\' && !inSingleQuote) { |
| 200 | // Skip the next character (it's escaped) |
| 201 | i++ |
| 202 | continue |
| 203 | } |
| 204 | |
| 205 | if (char === '"' && !inSingleQuote) { |
| 206 | inDoubleQuote = !inDoubleQuote |
| 207 | continue |
| 208 | } |
| 209 | |
| 210 | if (char === "'" && !inDoubleQuote) { |
| 211 | inSingleQuote = !inSingleQuote |
| 212 | |
| 213 | // Check if we just closed a single quote and the content ends with |
| 214 | // trailing backslashes. shell-quote's chunker regex '((\\'|[^'])*?)' |
| 215 | // incorrectly treats \' as an escape sequence inside single quotes, |
| 216 | // while bash treats backslash as literal. This creates a differential |
| 217 | // where shell-quote merges tokens that bash treats as separate. |
| 218 | // |
| 219 | // Odd trailing \'s = always a bug: |
| 220 | // '\' -> shell-quote: \' = literal ', still open. bash: \, closed. |
| 221 | // 'abc\' -> shell-quote: abc then \' = literal ', still open. bash: abc\, closed. |
| 222 | // '\\\' -> shell-quote: \\ + \', still open. bash: \\\, closed. |
| 223 | // |
| 224 | // Even trailing \'s = bug ONLY when a later ' exists in the command: |
| 225 | // '\\' alone -> shell-quote backtracks, both parsers agree string closes. OK. |
| 226 | // '\\' 'next' -> shell-quote: \' consumes the closing ', finds next ' as |
| 227 | // false close, merges tokens. bash: two separate tokens. |
| 228 | // |
| 229 | // Detail: the regex alternation tries \' before [^']. For '\\', it matches |
| 230 | // the first \ via [^'] (next char is \, not '), then the second \ via \' |
| 231 | // (next char IS '). This consumes the closing '. The regex continues reading |
| 232 | // until it finds another ' to close the match. If none exists, it backtracks |
| 233 | // to [^'] for the second \ and closes correctly. If a later ' exists (e.g., |
| 234 | // the opener of the next single-quoted arg), no backtracking occurs and |
| 235 | // tokens merge. See H1 report: git ls-remote 'safe\\' '--upload-pack=evil' 'repo' |
| 236 | // shell-quote: ["git","ls-remote","safe\\\\ --upload-pack=evil repo"] |
| 237 | // bash: ["git","ls-remote","safe\\\\","--upload-pack=evil","repo"] |
| 238 | if (!inSingleQuote) { |
| 239 | let backslashCount = 0 |
| 240 | let j = i - 1 |
| 241 | while (j >= 0 && command[j] === '\\') { |
| 242 | backslashCount++ |
| 243 | j-- |
| 244 | } |
| 245 | if (backslashCount > 0 && backslashCount % 2 === 1) { |
| 246 | return true |
| 247 | } |
no outgoing calls
no test coverage detected