* Check if a command_substitution node is exactly `$(cat <<'DELIM'...DELIM)` * and return the heredoc body if so. Any deviation (extra args to cat, * unquoted delimiter, additional commands) returns null. * * tree-sitter structure: * command_substitution * $( * redirected_statement
(subNode: Node)
| 1719 | * ) |
| 1720 | */ |
| 1721 | function extractSafeCatHeredoc(subNode: Node): string | 'DANGEROUS' | null { |
| 1722 | // Expect exactly: $( + one redirected_statement + ) |
| 1723 | let stmt: Node | null = null |
| 1724 | for (const child of subNode.children) { |
| 1725 | if (!child) continue |
| 1726 | if (child.type === '$(' || child.type === ')') continue |
| 1727 | if (child.type === 'redirected_statement' && stmt === null) { |
| 1728 | stmt = child |
| 1729 | } else { |
| 1730 | return null |
| 1731 | } |
| 1732 | } |
| 1733 | if (!stmt) return null |
| 1734 | |
| 1735 | // redirected_statement must be: command(cat) + heredoc_redirect (quoted) |
| 1736 | let sawCat = false |
| 1737 | let body: string | null = null |
| 1738 | for (const child of stmt.children) { |
| 1739 | if (!child) continue |
| 1740 | if (child.type === 'command') { |
| 1741 | // Must be bare `cat` — no args, no env vars |
| 1742 | const cmdChildren = child.children.filter(c => c) |
| 1743 | if (cmdChildren.length !== 1) return null |
| 1744 | const nameNode = cmdChildren[0] |
| 1745 | if (nameNode?.type !== 'command_name' || nameNode.text !== 'cat') { |
| 1746 | return null |
| 1747 | } |
| 1748 | sawCat = true |
| 1749 | } else if (child.type === 'heredoc_redirect') { |
| 1750 | // Reuse the existing validator: quoted delimiter, body is pure text. |
| 1751 | // walkHeredocRedirect returns null on success, non-null on rejection. |
| 1752 | if (walkHeredocRedirect(child) !== null) return null |
| 1753 | for (const hc of child.children) { |
| 1754 | if (hc?.type === 'heredoc_body') body = hc.text |
| 1755 | } |
| 1756 | } else { |
| 1757 | return null |
| 1758 | } |
| 1759 | } |
| 1760 | |
| 1761 | if (!sawCat || body === null) return null |
| 1762 | // SECURITY: the heredoc body becomes the outer command's argv value via |
| 1763 | // substitution, so a body like `/proc/self/environ` is semantically |
| 1764 | // `cat /proc/self/environ`. checkSemantics never sees the body (we drop it |
| 1765 | // at the walkString call site to avoid newline+# FPs). Returning `null` |
| 1766 | // here would fall through to collectCommandSubstitution in walkString, |
| 1767 | // which would extract the inner `cat` via walkHeredocRedirect (body text |
| 1768 | // not inspected there) — effectively bypassing this check. Return a |
| 1769 | // distinct sentinel so the caller can reject instead of falling through. |
| 1770 | if (PROC_ENVIRON_RE.test(body)) return 'DANGEROUS' |
| 1771 | // Same for jq system(): checkSemantics checks argv but never sees the |
| 1772 | // heredoc body. Check unconditionally (we don't know the outer command). |
| 1773 | if (/\bsystem\s*\(/.test(body)) return 'DANGEROUS' |
| 1774 | return body |
| 1775 | } |
| 1776 | |
| 1777 | function walkVariableAssignment( |
| 1778 | node: Node, |
no test coverage detected