(command: string)
| 537 | |
| 538 | // Checks if this is just a list of commands |
| 539 | function isCommandList(command: string): boolean { |
| 540 | // Generate unique placeholders for this parse to prevent injection attacks |
| 541 | const placeholders = generatePlaceholders() |
| 542 | |
| 543 | // Extract heredocs before parsing - shell-quote parses << incorrectly |
| 544 | const { processedCommand } = extractHeredocs(command) |
| 545 | |
| 546 | const parseResult = tryParseShellCommand( |
| 547 | processedCommand |
| 548 | .replaceAll('"', `"${placeholders.DOUBLE_QUOTE}`) // parse() strips out quotes :P |
| 549 | .replaceAll("'", `'${placeholders.SINGLE_QUOTE}`), // parse() strips out quotes :P |
| 550 | varName => `$${varName}`, // Preserve shell variables |
| 551 | ) |
| 552 | |
| 553 | // If parse failed, it's not a safe command list |
| 554 | if (!parseResult.success) { |
| 555 | return false |
| 556 | } |
| 557 | |
| 558 | const parts = parseResult.tokens |
| 559 | for (let i = 0; i < parts.length; i++) { |
| 560 | const part = parts[i] |
| 561 | const nextPart = parts[i + 1] |
| 562 | if (part === undefined) { |
| 563 | continue |
| 564 | } |
| 565 | |
| 566 | if (typeof part === 'string') { |
| 567 | // Strings are safe |
| 568 | continue |
| 569 | } |
| 570 | if ('comment' in part) { |
| 571 | // Don't trust comments, they can contain command injection |
| 572 | return false |
| 573 | } |
| 574 | if ('op' in part) { |
| 575 | if (part.op === 'glob') { |
| 576 | // Globs are safe |
| 577 | continue |
| 578 | } else if (COMMAND_LIST_SEPARATORS.has(part.op)) { |
| 579 | // Command list separators are safe |
| 580 | continue |
| 581 | } else if (part.op === '>&') { |
| 582 | // Redirection to standard input/output/error file descriptors is safe |
| 583 | if ( |
| 584 | nextPart !== undefined && |
| 585 | typeof nextPart === 'string' && |
| 586 | ALLOWED_FILE_DESCRIPTORS.has(nextPart.trim()) |
| 587 | ) { |
| 588 | continue |
| 589 | } |
| 590 | } else if (part.op === '>') { |
| 591 | // Output redirections are validated by pathValidation.ts |
| 592 | continue |
| 593 | } else if (part.op === '>>') { |
| 594 | // Append redirections are validated by pathValidation.ts |
| 595 | continue |
| 596 | } |
no test coverage detected