Check if a shell command is in the allow-list. The allow-list matches against the first token of the command (the executable name). This allows read-only commands like ls, cat, grep, etc. to be auto-approved. When `allow_list` is the `SHELL_ALLOW_ALL` sentinel, all non-empty comman
(command: str, allow_list: list[str] | None)
| 2783 | |
| 2784 | |
| 2785 | def is_shell_command_allowed(command: str, allow_list: list[str] | None) -> bool: |
| 2786 | """Check if a shell command is in the allow-list. |
| 2787 | |
| 2788 | The allow-list matches against the first token of the command (the executable |
| 2789 | name). This allows read-only commands like ls, cat, grep, etc. to be |
| 2790 | auto-approved. |
| 2791 | |
| 2792 | When `allow_list` is the `SHELL_ALLOW_ALL` sentinel, all non-empty commands |
| 2793 | are approved unconditionally — dangerous pattern checks are skipped. |
| 2794 | |
| 2795 | SECURITY: For regular allow-lists, this function rejects commands containing |
| 2796 | dangerous shell patterns (command substitution, redirects, process |
| 2797 | substitution, etc.) BEFORE parsing, to prevent injection attacks that could |
| 2798 | bypass the allow-list. |
| 2799 | |
| 2800 | Args: |
| 2801 | command: The full shell command to check. |
| 2802 | allow_list: List of allowed command names (e.g., `["ls", "cat", "grep"]`), |
| 2803 | the `SHELL_ALLOW_ALL` sentinel to allow any command, or `None`. |
| 2804 | |
| 2805 | Returns: |
| 2806 | `True` if the command is allowed, `False` otherwise. |
| 2807 | """ |
| 2808 | if not allow_list or not command or not command.strip(): |
| 2809 | return False |
| 2810 | |
| 2811 | # SHELL_ALLOW_ALL sentinel — skip pattern and token checks |
| 2812 | if isinstance(allow_list, _ShellAllowAll): |
| 2813 | return True |
| 2814 | |
| 2815 | # SECURITY: Check for dangerous patterns BEFORE any parsing |
| 2816 | # This prevents injection attacks like: ls "$(rm -rf /)" |
| 2817 | if contains_dangerous_patterns(command): |
| 2818 | return False |
| 2819 | |
| 2820 | allow_set = set(allow_list) |
| 2821 | |
| 2822 | # Extract the first command token |
| 2823 | # Handle pipes and other shell operators by checking each command in the pipeline |
| 2824 | # Split by compound operators first (&&, ||), then single-char operators (|, ;). |
| 2825 | # Note: standalone & (background) is blocked by contains_dangerous_patterns above. |
| 2826 | segments = re.split(r"&&|\|\||[|;]", command) |
| 2827 | |
| 2828 | # Track if we found at least one valid command |
| 2829 | found_command = False |
| 2830 | |
| 2831 | for raw_segment in segments: |
| 2832 | segment = raw_segment.strip() |
| 2833 | if not segment: |
| 2834 | continue |
| 2835 | |
| 2836 | try: |
| 2837 | # Try to parse as shell command to extract the executable name |
| 2838 | tokens = shlex.split(segment) |
| 2839 | if tokens: |
| 2840 | found_command = True |
| 2841 | cmd_name = tokens[0] |
| 2842 | # Check if this command is in the allow set |