* Detects unquoted brace expansion syntax that Bash expands but shell-quote/tree-sitter * treat as literal strings. This parsing discrepancy allows permission bypass: * git ls-remote {--upload-pack="touch /tmp/test",test} * Parser sees one literal arg, but Bash expands to: --upload-pack="touch
(context: ValidationContext)
| 1749 | * Backslash-escaped braces (\{, \}) also suppress expansion. |
| 1750 | */ |
| 1751 | function validateBraceExpansion(context: ValidationContext): PermissionResult { |
| 1752 | // Use pre-strip content to avoid false negatives from stripSafeRedirections |
| 1753 | // creating backslash adjacencies (e.g., `\>/dev/null{a,b}` → `\{a,b}` after |
| 1754 | // stripping, making isEscapedAtPosition think the brace is escaped). |
| 1755 | const content = context.fullyUnquotedPreStrip |
| 1756 | |
| 1757 | // SECURITY: Check for MISMATCHED brace counts in fullyUnquoted content. |
| 1758 | // A mismatch indicates that quoted braces (e.g., `'{'` or `"{"`) were |
| 1759 | // stripped by extractQuotedContent, leaving unbalanced braces in the content |
| 1760 | // we analyze. Our depth-matching algorithm below assumes balanced braces — |
| 1761 | // with a mismatch, it closes at the WRONG position, missing commas that |
| 1762 | // bash's algorithm WOULD find. |
| 1763 | // |
| 1764 | // Exploit: `git diff {@'{'0},--output=/tmp/pwned}` |
| 1765 | // - Original: 2 `{`, 2 `}` (quoted `'{'` counts as content, not operator) |
| 1766 | // - fullyUnquoted: `git diff {@0},--output=/tmp/pwned}` — 1 `{`, 2 `}`! |
| 1767 | // - Our depth-matcher: closes at first `}` (after `0`), inner=`@0`, no `,` |
| 1768 | // - Bash (on original): quoted `{` is content; first unquoted `}` has no |
| 1769 | // `,` yet → bash treats as literal content, keeps scanning → finds `,` |
| 1770 | // → final `}` closes → expands to `@{0} --output=/tmp/pwned` |
| 1771 | // - git writes diff to /tmp/pwned. ARBITRARY FILE WRITE, ZERO PERMISSIONS. |
| 1772 | // |
| 1773 | // We count ONLY unescaped braces (backslash-escaped braces are literal in |
| 1774 | // bash). If counts mismatch AND at least one unescaped `{` exists, block — |
| 1775 | // our depth-matching cannot be trusted on this content. |
| 1776 | let unescapedOpenBraces = 0 |
| 1777 | let unescapedCloseBraces = 0 |
| 1778 | for (let i = 0; i < content.length; i++) { |
| 1779 | if (content[i] === '{' && !isEscapedAtPosition(content, i)) { |
| 1780 | unescapedOpenBraces++ |
| 1781 | } else if (content[i] === '}' && !isEscapedAtPosition(content, i)) { |
| 1782 | unescapedCloseBraces++ |
| 1783 | } |
| 1784 | } |
| 1785 | // Only block when CLOSE count EXCEEDS open count — this is the specific |
| 1786 | // attack signature. More `}` than `{` means a quoted `{` was stripped |
| 1787 | // (bash saw it as content, we see extra `}` unaccounted for). The inverse |
| 1788 | // (more `{` than `}`) is usually legitimate unclosed/escaped braces like |
| 1789 | // `{foo` or `{a,b\}` where bash doesn't expand anyway. |
| 1790 | if (unescapedOpenBraces > 0 && unescapedCloseBraces > unescapedOpenBraces) { |
| 1791 | logEvent('tengu_bash_security_check_triggered', { |
| 1792 | checkId: BASH_SECURITY_CHECK_IDS.BRACE_EXPANSION, |
| 1793 | subId: 2, |
| 1794 | }) |
| 1795 | return { |
| 1796 | behavior: 'ask', |
| 1797 | message: |
| 1798 | 'Command has excess closing braces after quote stripping, indicating possible brace expansion obfuscation', |
| 1799 | } |
| 1800 | } |
| 1801 | |
| 1802 | // SECURITY: Additionally, check the ORIGINAL command (before quote stripping) |
| 1803 | // for `'{'` or `"{"` INSIDE an unquoted brace context — this is the specific |
| 1804 | // attack primitive. A quoted brace inside an outer unquoted `{...}` is |
| 1805 | // essentially always an obfuscation attempt; legitimate commands don't nest |
| 1806 | // quoted braces inside brace expansion (awk/find patterns are fully quoted, |
| 1807 | // like `awk '{print $1}'` where the OUTER brace is inside quotes too). |
| 1808 | // |
nothing calls this directly
no test coverage detected