(command: string)
| 1629 | const SHELL_OPERATORS = new Set([';', '|', '&', '<', '>']) |
| 1630 | |
| 1631 | function hasBackslashEscapedOperator(command: string): boolean { |
| 1632 | let inSingleQuote = false |
| 1633 | let inDoubleQuote = false |
| 1634 | |
| 1635 | for (let i = 0; i < command.length; i++) { |
| 1636 | const char = command[i] |
| 1637 | |
| 1638 | // SECURITY: Handle backslash FIRST, before quote toggles. In bash, inside |
| 1639 | // double quotes, `\"` is an escape sequence producing a literal `"` — it |
| 1640 | // does NOT close the quote. If we process quote toggles first, `\"` inside |
| 1641 | // `"..."` desyncs the tracker: |
| 1642 | // - `\` is ignored (gated by !inDoubleQuote) |
| 1643 | // - `"` toggles inDoubleQuote to FALSE (wrong — bash says still inside) |
| 1644 | // - next `"` (the real closing quote) toggles BACK to TRUE — locked desync |
| 1645 | // - subsequent `\;` is missed because !inDoubleQuote is false |
| 1646 | // Exploit: `tac "x\"y" \; echo ~/.ssh/id_rsa` — bash runs ONE tac reading |
| 1647 | // all args as files (leaking id_rsa), but desynced tracker misses `\;` and |
| 1648 | // splitCommand's double-parse normalization "sees" two safe commands. |
| 1649 | // |
| 1650 | // Fix structure matches hasBackslashEscapedWhitespace (which was correctly |
| 1651 | // fixed for this in commit prior to d000dfe84e): backslash check first, |
| 1652 | // gated only by !inSingleQuote (since backslash IS literal inside '...'), |
| 1653 | // unconditional i++ to skip the escaped char even inside double quotes. |
| 1654 | if (char === '\\' && !inSingleQuote) { |
| 1655 | // Only flag \<operator> when OUTSIDE double quotes (inside double quotes, |
| 1656 | // operators like ;|&<> are already not special, so \; is harmless there). |
| 1657 | if (!inDoubleQuote) { |
| 1658 | const nextChar = command[i + 1] |
| 1659 | if (nextChar && SHELL_OPERATORS.has(nextChar)) { |
| 1660 | return true |
| 1661 | } |
| 1662 | } |
| 1663 | // Skip the escaped character unconditionally. Inside double quotes, this |
| 1664 | // correctly consumes backslash pairs: `"x\\"` → pos 6 (`\`) skips pos 7 |
| 1665 | // (`\`), then pos 8 (`"`) toggles inDoubleQuote off correctly. Without |
| 1666 | // unconditional skip, pos 7 would see `\`, see pos 8 (`"`) as nextChar, |
| 1667 | // skip it, and the closing quote would NEVER toggle inDoubleQuote — |
| 1668 | // permanently desyncing and missing subsequent `\;` outside quotes. |
| 1669 | // Exploit: `cat "x\\" \; echo /etc/passwd` — bash reads /etc/passwd. |
| 1670 | // |
| 1671 | // This correctly handles backslash parity: odd-count `\;` (1, 3, 5...) |
| 1672 | // is flagged (the unpaired `\` before `;` is detected). Even-count `\\;` |
| 1673 | // (2, 4...) is NOT flagged, which is CORRECT — bash treats `\\` as |
| 1674 | // literal `\` and `;` as a separator, so splitCommand handles it |
| 1675 | // normally (no double-parse bug). This matches |
| 1676 | // hasBackslashEscapedWhitespace line ~1340. |
| 1677 | i++ |
| 1678 | continue |
| 1679 | } |
| 1680 | |
| 1681 | // Quote toggles come AFTER backslash handling (backslash already skipped |
| 1682 | // any escaped quote char, so these toggles only fire on unescaped quotes). |
| 1683 | if (char === "'" && !inDoubleQuote) { |
| 1684 | inSingleQuote = !inSingleQuote |
| 1685 | continue |
| 1686 | } |
| 1687 | if (char === '"' && !inSingleQuote) { |
| 1688 | inDoubleQuote = !inDoubleQuote |
no test coverage detected