* Checks if a single command string is read-only based on READONLY_COMMAND_REGEXES. * Internal helper function that validates individual commands. * * @param command The command string to check * @returns true if the command is read-only
(command: string)
| 1676 | * @returns true if the command is read-only |
| 1677 | */ |
| 1678 | function isCommandReadOnly(command: string): boolean { |
| 1679 | // Handle common stderr-to-stdout redirection pattern |
| 1680 | // This handles both "command 2>&1" at the end of a full command |
| 1681 | // and "command 2>&1" as part of a pipeline component |
| 1682 | let testCommand = command.trim() |
| 1683 | if (testCommand.endsWith(' 2>&1')) { |
| 1684 | // Remove the stderr redirection for pattern matching |
| 1685 | testCommand = testCommand.slice(0, -5).trim() |
| 1686 | } |
| 1687 | |
| 1688 | // Check for Windows UNC paths that could be vulnerable to WebDAV attacks |
| 1689 | // Do this early to prevent any command with UNC paths from being marked as read-only |
| 1690 | if (containsVulnerableUncPath(testCommand)) { |
| 1691 | return false |
| 1692 | } |
| 1693 | |
| 1694 | // Check for unquoted glob characters and expandable `$` variables that could |
| 1695 | // bypass our regex-based security checks. We can't know what these expand to |
| 1696 | // at runtime, so we can't verify the command is read-only. |
| 1697 | // |
| 1698 | // Globs: `python *` could expand to `python --help` if such a file exists. |
| 1699 | // |
| 1700 | // Variables: `uniq --skip-chars=0$_` — bash expands `$_` at runtime to the |
| 1701 | // last arg of the previous command. With IFS word splitting, this smuggles |
| 1702 | // positional args past "flags-only" regexes like uniq's `\S+`. The `$` token |
| 1703 | // check inside isCommandSafeViaFlagParsing only covers COMMAND_ALLOWLIST |
| 1704 | // commands; hand-written regexes in READONLY_COMMAND_REGEXES (uniq, jq, cd) |
| 1705 | // have no such guard. See containsUnquotedExpansion for full analysis. |
| 1706 | if (containsUnquotedExpansion(testCommand)) { |
| 1707 | return false |
| 1708 | } |
| 1709 | |
| 1710 | // Tools like git allow `--upload-pack=cmd` to be abbreviated as `--up=cmd` |
| 1711 | // Regex filters can be bypassed, so we use strict allowlist validation instead. |
| 1712 | // This requires defining a set of known safe flags. Claude can help with this, |
| 1713 | // but please look over it to ensure it didn't add any flags that allow file writes |
| 1714 | // code execution, or network requests. |
| 1715 | if (isCommandSafeViaFlagParsing(testCommand)) { |
| 1716 | return true |
| 1717 | } |
| 1718 | |
| 1719 | for (const regex of READONLY_COMMAND_REGEXES) { |
| 1720 | if (regex.test(testCommand)) { |
| 1721 | // Prevent git commands with -c flag to avoid config options that can lead to code execution |
| 1722 | // The -c flag allows setting arbitrary git config values inline, including dangerous ones like |
| 1723 | // core.fsmonitor, diff.external, core.gitProxy, etc. that can execute arbitrary commands |
| 1724 | // Check for -c preceded by whitespace and followed by whitespace or equals |
| 1725 | // Using regex to catch spaces, tabs, and other whitespace (not part of other flags like --cached) |
| 1726 | if (testCommand.includes('git') && /\s-c[\s=]/.test(testCommand)) { |
| 1727 | return false |
| 1728 | } |
| 1729 | |
| 1730 | // Prevent git commands with --exec-path flag to avoid path manipulation that can lead to code execution |
| 1731 | // The --exec-path flag allows overriding the directory where git looks for executables |
| 1732 | if ( |
| 1733 | testCommand.includes('git') && |
| 1734 | /\s--exec-path[\s=]/.test(testCommand) |
| 1735 | ) { |
no test coverage detected