( node: Node, innerCommands: SimpleCommand[], varScope: Map<string, string>, )
| 1775 | } |
| 1776 | |
| 1777 | function walkVariableAssignment( |
| 1778 | node: Node, |
| 1779 | innerCommands: SimpleCommand[], |
| 1780 | varScope: Map<string, string>, |
| 1781 | ): { name: string; value: string; isAppend: boolean } | ParseForSecurityResult { |
| 1782 | let name: string | null = null |
| 1783 | let value = '' |
| 1784 | let isAppend = false |
| 1785 | |
| 1786 | for (const child of node.children) { |
| 1787 | if (!child) continue |
| 1788 | if (child.type === 'variable_name') { |
| 1789 | name = child.text |
| 1790 | } else if (child.type === '=' || child.type === '+=') { |
| 1791 | // `PATH+=":/new"` — tree-sitter emits `+=` as a distinct operator |
| 1792 | // node. Without this case it falls through to walkArgument below |
| 1793 | // → tooComplex on unknown type `+=`. |
| 1794 | isAppend = child.type === '+=' |
| 1795 | continue |
| 1796 | } else if (child.type === 'command_substitution') { |
| 1797 | // $() as the variable's value. The output becomes a STRING stored in |
| 1798 | // the variable — it's NOT a positional argument (no path/flag concern). |
| 1799 | // `VAR=$(date)` runs `date`, stores output. `VAR=$(rm -rf /)` runs |
| 1800 | // `rm` — the inner command IS checked against permission rules, so |
| 1801 | // `rm` must match a rule. The variable just holds whatever `rm` prints. |
| 1802 | const err = collectCommandSubstitution(child, innerCommands, varScope) |
| 1803 | if (err) return err |
| 1804 | value = CMDSUB_PLACEHOLDER |
| 1805 | } else if (child.type === 'simple_expansion') { |
| 1806 | // `VAR=$OTHER` — assignment RHS does NOT word-split or glob-expand |
| 1807 | // in bash (unlike command arguments). So `A="a b"; B=$A` sets B to |
| 1808 | // the literal "a b". Resolve as if inside a string (insideString=true) |
| 1809 | // so BARE_VAR_UNSAFE_RE doesn't over-reject. The resulting value may |
| 1810 | // contain spaces/globs — if B is later used as a bare arg, THAT use |
| 1811 | // will correctly reject via BARE_VAR_UNSAFE_RE. |
| 1812 | const v = resolveSimpleExpansion(child, varScope, true) |
| 1813 | if (typeof v !== 'string') return v |
| 1814 | // If v is VAR_PLACEHOLDER (OTHER holds unknown), store it — combined |
| 1815 | // with containsAnyPlaceholder in the caller to treat as unknown. |
| 1816 | value = v |
| 1817 | } else { |
| 1818 | const v = walkArgument(child, innerCommands, varScope) |
| 1819 | if (typeof v !== 'string') return v |
| 1820 | value = v |
| 1821 | } |
| 1822 | } |
| 1823 | |
| 1824 | if (name === null) { |
| 1825 | return { |
| 1826 | kind: 'too-complex', |
| 1827 | reason: 'Variable assignment without name', |
| 1828 | nodeType: 'variable_assignment', |
| 1829 | } |
| 1830 | } |
| 1831 | // SECURITY: tree-sitter-bash accepts invalid var names (e.g. `1VAR=value`) |
| 1832 | // as variable_assignment. Bash only recognizes [A-Za-z_][A-Za-z0-9_]* — |
| 1833 | // anything else is run as a COMMAND. `1VAR=value` → bash tries to execute |
| 1834 | // `1VAR=value` from PATH. We must not treat it as an inert assignment. |
no test coverage detected