* Convert an argument node to its literal string value. Quotes are resolved. * This function implements the argument-position allowlist.
( node: Node | null, innerCommands: SimpleCommand[], varScope: Map<string, string>, )
| 1397 | * This function implements the argument-position allowlist. |
| 1398 | */ |
| 1399 | function walkArgument( |
| 1400 | node: Node | null, |
| 1401 | innerCommands: SimpleCommand[], |
| 1402 | varScope: Map<string, string>, |
| 1403 | ): string | ParseForSecurityResult { |
| 1404 | if (!node) { |
| 1405 | return { kind: 'too-complex', reason: 'Null argument node' } |
| 1406 | } |
| 1407 | |
| 1408 | switch (node.type) { |
| 1409 | case 'word': { |
| 1410 | // Unescape backslash sequences. In unquoted context, bash's quote |
| 1411 | // removal turns `\X` → `X` for any character X. tree-sitter preserves |
| 1412 | // the raw text. Required for checkSemantics: `\eval` must match |
| 1413 | // EVAL_LIKE_BUILTINS, `\zmodload` must match ZSH_DANGEROUS_BUILTINS. |
| 1414 | // Also makes argv accurate: `find -exec {} \;` → argv has `;` not |
| 1415 | // `\;`. (Deny-rule matching on .text already worked via downstream |
| 1416 | // splitCommand_DEPRECATED unescaping — see walkCommand comment.) `\<whitespace>` |
| 1417 | // is already rejected by BACKSLASH_WHITESPACE_RE. |
| 1418 | if (BRACE_EXPANSION_RE.test(node.text)) { |
| 1419 | return { |
| 1420 | kind: 'too-complex', |
| 1421 | reason: 'Word contains brace expansion syntax', |
| 1422 | nodeType: 'word', |
| 1423 | } |
| 1424 | } |
| 1425 | return node.text.replace(/\\(.)/g, '$1') |
| 1426 | } |
| 1427 | |
| 1428 | case 'number': |
| 1429 | // SECURITY: tree-sitter-bash parses `NN#<expansion>` (arithmetic base |
| 1430 | // syntax) as a `number` node with the expansion as a CHILD. `10#$(cmd)` |
| 1431 | // is a number node whose .text is the full literal but whose child is a |
| 1432 | // command_substitution — bash runs the substitution. .text on a node |
| 1433 | // with children would smuggle the expansion past permission checks. |
| 1434 | // Plain numbers (`10`, `16#ff`) have zero children. |
| 1435 | if (node.children.length > 0) { |
| 1436 | return { |
| 1437 | kind: 'too-complex', |
| 1438 | reason: 'Number node contains expansion (NN# arithmetic base syntax)', |
| 1439 | nodeType: node.children[0]?.type, |
| 1440 | } |
| 1441 | } |
| 1442 | return node.text |
| 1443 | |
| 1444 | case 'raw_string': |
| 1445 | return stripRawString(node.text) |
| 1446 | |
| 1447 | case 'string': |
| 1448 | return walkString(node, innerCommands, varScope) |
| 1449 | |
| 1450 | case 'concatenation': { |
| 1451 | if (BRACE_EXPANSION_RE.test(node.text)) { |
| 1452 | return { |
| 1453 | kind: 'too-complex', |
| 1454 | reason: 'Brace expansion', |
| 1455 | nodeType: 'concatenation', |
| 1456 | } |
no test coverage detected