* Checks if a redirection target is a simple static file path that can be safely stripped. * Returns false for targets containing dynamic content (variables, command substitutions, globs, * shell expansions) which should remain visible in permission prompts for security.
(target: string)
| 45 | * shell expansions) which should remain visible in permission prompts for security. |
| 46 | */ |
| 47 | function isStaticRedirectTarget(target: string): boolean { |
| 48 | // SECURITY: A static redirect target in bash is a SINGLE shell word. After |
| 49 | // the adjacent-string collapse at splitCommandWithOperators, multiple args |
| 50 | // following a redirect get merged into one string with spaces. For |
| 51 | // `cat > out /etc/passwd`, bash writes to `out` and reads `/etc/passwd`, |
| 52 | // but the collapse gives us `out /etc/passwd` as the "target". Accepting |
| 53 | // this merged blob returns `['cat']` and pathValidation never sees the path. |
| 54 | // Reject any target containing whitespace or quote chars (quotes indicate |
| 55 | // the placeholder-restoration preserved a quoted arg). |
| 56 | if (/[\s'"]/.test(target)) return false |
| 57 | // Reject empty string — path.resolve(cwd, '') returns cwd (always allowed). |
| 58 | if (target.length === 0) return false |
| 59 | // SECURITY (parser differential hardening): shell-quote parses `#foo` at |
| 60 | // word-initial position as a comment token. In bash, `#` after whitespace |
| 61 | // also starts a comment (`> #file` is a syntax error). But shell-quote |
| 62 | // returns it as a comment OBJECT; splitCommandWithOperators maps it back to |
| 63 | // string `#foo`. This differs from extractOutputRedirections (which sees the |
| 64 | // comment object as non-string, missing the target). While `> #file` is |
| 65 | // unexecutable in bash, rejecting `#`-prefixed targets closes the differential. |
| 66 | if (target.startsWith('#')) return false |
| 67 | return ( |
| 68 | !target.startsWith('!') && // No history expansion like !!, !-1, !foo |
| 69 | !target.startsWith('=') && // No Zsh equals expansion (=cmd expands to /path/to/cmd) |
| 70 | !target.includes('$') && // No variables like $HOME |
| 71 | !target.includes('`') && // No command substitution like `pwd` |
| 72 | !target.includes('*') && // No glob patterns |
| 73 | !target.includes('?') && // No single-char glob |
| 74 | !target.includes('[') && // No character class glob |
| 75 | !target.includes('{') && // No brace expansion like {1,2} |
| 76 | !target.includes('~') && // No tilde expansion |
| 77 | !target.includes('(') && // No process substitution like >(cmd) |
| 78 | !target.includes('<') && // No process substitution like <(cmd) |
| 79 | !target.startsWith('&') // Not a file descriptor like &1 |
| 80 | ) |
| 81 | } |
| 82 | |
| 83 | export type { CommandPrefixResult, CommandSubcommandPrefixResult } |
| 84 |
no outgoing calls
no test coverage detected