( command: string, blocklist?: RegExp, )
| 731 | * BINARY_HIJACK_VARS for excludedCommands. |
| 732 | */ |
| 733 | export function stripAllLeadingEnvVars( |
| 734 | command: string, |
| 735 | blocklist?: RegExp, |
| 736 | ): string { |
| 737 | // Broader value pattern for deny-rule stripping. Handles: |
| 738 | // |
| 739 | // - Standard assignment (FOO=bar), append (FOO+=bar), array (FOO[0]=bar) |
| 740 | // - Single-quoted values: '[^'\n\r]*' — bash suppresses all expansion |
| 741 | // - Double-quoted values with backslash escapes: "(?:\\.|[^"$`\\\n\r])*" |
| 742 | // In bash double quotes, only \$, \`, \", \\, and \newline are special. |
| 743 | // Other \x sequences are harmless, so we allow \. inside double quotes. |
| 744 | // We still exclude raw $ and ` (without backslash) to block expansion. |
| 745 | // - Unquoted values: excludes shell metacharacters, allows backslash escapes |
| 746 | // - Concatenated segments: FOO='x'y"z" — bash concatenates adjacent segments |
| 747 | // |
| 748 | // SECURITY: Trailing whitespace MUST be [ \t]+ (horizontal only), NOT \s+. |
| 749 | // |
| 750 | // The outer * matches one atomic unit per iteration: a complete quoted |
| 751 | // string, a backslash-escape pair, or a single unquoted safe character. |
| 752 | // The inner double-quote alternation (?:...|...)* is bounded by the |
| 753 | // closing ", so it cannot interact with the outer * for backtracking. |
| 754 | // |
| 755 | // Note: $ is excluded from unquoted/double-quoted value classes to block |
| 756 | // dangerous forms like $(cmd), ${var}, and $((expr)). This means |
| 757 | // FOO=$VAR is not stripped — adding $VAR matching creates ReDoS risk |
| 758 | // (CodeQL #671) and $VAR bypasses are low-priority. |
| 759 | const ENV_VAR_PATTERN = |
| 760 | /^([A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?)\+?=(?:'[^'\n\r]*'|"(?:\\.|[^"$`\\\n\r])*"|\\.|[^ \t\n\r$`;|&()<>\\\\'"])*[ \t]+/ |
| 761 | |
| 762 | let stripped = command |
| 763 | let previousStripped = '' |
| 764 | |
| 765 | while (stripped !== previousStripped) { |
| 766 | previousStripped = stripped |
| 767 | stripped = stripCommentLines(stripped) |
| 768 | |
| 769 | const m = stripped.match(ENV_VAR_PATTERN) |
| 770 | if (!m) continue |
| 771 | if (blocklist?.test(m[1]!)) break |
| 772 | stripped = stripped.slice(m[0].length) |
| 773 | } |
| 774 | |
| 775 | return stripped.trim() |
| 776 | } |
| 777 | |
| 778 | function filterRulesByContentsMatchingInput( |
| 779 | input: z.infer<typeof BashTool.inputSchema>, |
no test coverage detected