( cmd: string, root: Node | typeof PARSE_ABORTED, )
| 398 | * successful parse doesn't. |
| 399 | */ |
| 400 | export function parseForSecurityFromAst( |
| 401 | cmd: string, |
| 402 | root: Node | typeof PARSE_ABORTED, |
| 403 | ): ParseForSecurityResult { |
| 404 | // Pre-checks: characters that cause tree-sitter and bash to disagree on |
| 405 | // word boundaries. These run before tree-sitter because they're the known |
| 406 | // tree-sitter/bash differentials. Everything after this point trusts |
| 407 | // tree-sitter's tokenization. |
| 408 | if (CONTROL_CHAR_RE.test(cmd)) { |
| 409 | return { kind: 'too-complex', reason: 'Contains control characters' } |
| 410 | } |
| 411 | if (UNICODE_WHITESPACE_RE.test(cmd)) { |
| 412 | return { kind: 'too-complex', reason: 'Contains Unicode whitespace' } |
| 413 | } |
| 414 | if (BACKSLASH_WHITESPACE_RE.test(cmd)) { |
| 415 | return { |
| 416 | kind: 'too-complex', |
| 417 | reason: 'Contains backslash-escaped whitespace', |
| 418 | } |
| 419 | } |
| 420 | if (ZSH_TILDE_BRACKET_RE.test(cmd)) { |
| 421 | return { |
| 422 | kind: 'too-complex', |
| 423 | reason: 'Contains zsh ~[ dynamic directory syntax', |
| 424 | } |
| 425 | } |
| 426 | if (ZSH_EQUALS_EXPANSION_RE.test(cmd)) { |
| 427 | return { |
| 428 | kind: 'too-complex', |
| 429 | reason: 'Contains zsh =cmd equals expansion', |
| 430 | } |
| 431 | } |
| 432 | if (BRACE_WITH_QUOTE_RE.test(maskBracesInQuotedContexts(cmd))) { |
| 433 | return { |
| 434 | kind: 'too-complex', |
| 435 | reason: 'Contains brace with quote character (expansion obfuscation)', |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | const trimmed = cmd.trim() |
| 440 | if (trimmed === '') { |
| 441 | return { kind: 'simple', commands: [] } |
| 442 | } |
| 443 | |
| 444 | if (root === PARSE_ABORTED) { |
| 445 | // SECURITY: module loaded but parse aborted (timeout / node budget / |
| 446 | // panic). Adversarially triggerable — `(( a[0][0]... ))` with ~2800 |
| 447 | // subscripts hits PARSE_TIMEOUT_MICROS under the 10K length limit. |
| 448 | // Previously indistinguishable from module-not-loaded → routed to |
| 449 | // legacy (parse-unavailable), which lacks EVAL_LIKE_BUILTINS — `trap`, |
| 450 | // `enable`, `hash` leaked with Bash(*). Fail closed: too-complex → ask. |
| 451 | return { |
| 452 | kind: 'too-complex', |
| 453 | reason: |
| 454 | 'Parser aborted (timeout or resource limit) — possible adversarial input', |
| 455 | nodeType: 'PARSE_ABORT', |
| 456 | } |
| 457 | } |
no test coverage detected