* Heredoc redirect. Only quoted-delimiter heredocs (<<'EOF') are safe — * their bodies are literal text. Unquoted-delimiter heredocs (<<EOF) * undergo full parameter/command/arithmetic expansion in the body. * * SECURITY: tree-sitter-bash has a grammar gap — backticks (`...`) inside * an unquot
(node: Node)
| 1141 | * <<'EOF' to get a literal body, which the model already prefers. |
| 1142 | */ |
| 1143 | function walkHeredocRedirect(node: Node): ParseForSecurityResult | null { |
| 1144 | let startText: string | null = null |
| 1145 | let body: Node | null = null |
| 1146 | |
| 1147 | for (const child of node.children) { |
| 1148 | if (!child) continue |
| 1149 | if (child.type === 'heredoc_start') startText = child.text |
| 1150 | else if (child.type === 'heredoc_body') body = child |
| 1151 | else if ( |
| 1152 | child.type === '<<' || |
| 1153 | child.type === '<<-' || |
| 1154 | child.type === 'heredoc_end' || |
| 1155 | child.type === 'file_descriptor' |
| 1156 | ) { |
| 1157 | // expected structural tokens — safe to skip. file_descriptor |
| 1158 | // covers fd-prefixed heredocs (`cat 3<<'EOF'`) — walkFileRedirect |
| 1159 | // already treats it as a benign structural token. |
| 1160 | } else { |
| 1161 | // SECURITY: tree-sitter places pipeline / command / file_redirect / |
| 1162 | // && / etc. as children of heredoc_redirect when they follow the |
| 1163 | // delimiter on the same line (e.g. `ls <<'EOF' | rm x`). Previously |
| 1164 | // these were silently skipped, hiding the piped command from |
| 1165 | // permission checks. Fail closed like every other walker. |
| 1166 | return tooComplex(child) |
| 1167 | } |
| 1168 | } |
| 1169 | |
| 1170 | const isQuoted = |
| 1171 | startText !== null && |
| 1172 | ((startText.startsWith("'") && startText.endsWith("'")) || |
| 1173 | (startText.startsWith('"') && startText.endsWith('"')) || |
| 1174 | startText.startsWith('\\')) |
| 1175 | |
| 1176 | if (!isQuoted) { |
| 1177 | return { |
| 1178 | kind: 'too-complex', |
| 1179 | reason: 'Heredoc with unquoted delimiter undergoes shell expansion', |
| 1180 | nodeType: 'heredoc_redirect', |
| 1181 | } |
| 1182 | } |
| 1183 | |
| 1184 | if (body) { |
| 1185 | for (const child of body.children) { |
| 1186 | if (!child) continue |
| 1187 | if (child.type !== 'heredoc_content') { |
| 1188 | return tooComplex(child) |
| 1189 | } |
| 1190 | } |
| 1191 | } |
| 1192 | return null |
| 1193 | } |
| 1194 | |
| 1195 | /** |
| 1196 | * Here-string redirect (`<<< content`). The content becomes stdin — not |
no test coverage detected