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