* SECURITY: Carriage return (\r, 0x0D) IS a misparsing concern, unlike LF. * * Parser differential: * - shell-quote's BAREWORD regex uses `[^\s...]` — JS `\s` INCLUDES \r, so * shell-quote treats CR as a token boundary. `TZ=UTC\recho` tokenizes as * TWO tokens: ['TZ=UTC', 'echo']. spl
(context: ValidationContext)
| 969 | * and shell-quote preserves the token (no split). |
| 970 | */ |
| 971 | function validateCarriageReturn(context: ValidationContext): PermissionResult { |
| 972 | const { originalCommand } = context |
| 973 | |
| 974 | if (!originalCommand.includes('\r')) { |
| 975 | return { behavior: 'passthrough', message: 'No carriage return' } |
| 976 | } |
| 977 | |
| 978 | // Check if CR appears outside double quotes. CR outside DQ (including inside |
| 979 | // SQ and unquoted) causes the shell-quote/bash tokenization differential. |
| 980 | let inSingleQuote = false |
| 981 | let inDoubleQuote = false |
| 982 | let escaped = false |
| 983 | for (let i = 0; i < originalCommand.length; i++) { |
| 984 | const c = originalCommand[i] |
| 985 | if (escaped) { |
| 986 | escaped = false |
| 987 | continue |
| 988 | } |
| 989 | if (c === '\\' && !inSingleQuote) { |
| 990 | escaped = true |
| 991 | continue |
| 992 | } |
| 993 | if (c === "'" && !inDoubleQuote) { |
| 994 | inSingleQuote = !inSingleQuote |
| 995 | continue |
| 996 | } |
| 997 | if (c === '"' && !inSingleQuote) { |
| 998 | inDoubleQuote = !inDoubleQuote |
| 999 | continue |
| 1000 | } |
| 1001 | if (c === '\r' && !inDoubleQuote) { |
| 1002 | logEvent('tengu_bash_security_check_triggered', { |
| 1003 | checkId: BASH_SECURITY_CHECK_IDS.NEWLINES, |
| 1004 | subId: 2, |
| 1005 | }) |
| 1006 | return { |
| 1007 | behavior: 'ask', |
| 1008 | message: |
| 1009 | 'Command contains carriage return (\\r) which shell-quote and bash tokenize differently', |
| 1010 | } |
| 1011 | } |
| 1012 | } |
| 1013 | |
| 1014 | return { behavior: 'passthrough', message: 'CR only inside double quotes' } |
| 1015 | } |
| 1016 | |
| 1017 | function validateIFSInjection(context: ValidationContext): PermissionResult { |
| 1018 | const { originalCommand } = context |
nothing calls this directly
no test coverage detected