* Detects a newline inside a quoted string where the NEXT line would be * stripped by stripCommentLines (trimmed line starts with `#`). * * In bash, `\n` inside quotes is a literal character and part of the argument. * But stripCommentLines (called by stripSafeWrappers in bashPermissions before
(context: ValidationContext)
| 2107 | * permission flow at bashPermissions.ts before any line-based processing runs. |
| 2108 | */ |
| 2109 | function validateQuotedNewline(context: ValidationContext): PermissionResult { |
| 2110 | const { originalCommand } = context |
| 2111 | |
| 2112 | // Fast path: must have both a newline byte AND a # character somewhere. |
| 2113 | // stripCommentLines only strips lines where trim().startsWith('#'), so |
| 2114 | // no # means no possible trigger. |
| 2115 | if (!originalCommand.includes('\n') || !originalCommand.includes('#')) { |
| 2116 | return { behavior: 'passthrough', message: 'No newline or no hash' } |
| 2117 | } |
| 2118 | |
| 2119 | // Track quote state. Mirrors extractQuotedContent / validateCommentQuoteDesync: |
| 2120 | // - single quotes don't toggle inside double quotes |
| 2121 | // - backslash escapes the next char (but not inside single quotes) |
| 2122 | // stripCommentLines splits on '\n' (not \r), so we only treat \n as a line |
| 2123 | // separator. \r inside a line is removed by trim() and doesn't change the |
| 2124 | // trimmed-starts-with-# check. |
| 2125 | let inSingleQuote = false |
| 2126 | let inDoubleQuote = false |
| 2127 | let escaped = false |
| 2128 | |
| 2129 | for (let i = 0; i < originalCommand.length; i++) { |
| 2130 | const char = originalCommand[i] |
| 2131 | |
| 2132 | if (escaped) { |
| 2133 | escaped = false |
| 2134 | continue |
| 2135 | } |
| 2136 | |
| 2137 | if (char === '\\' && !inSingleQuote) { |
| 2138 | escaped = true |
| 2139 | continue |
| 2140 | } |
| 2141 | |
| 2142 | if (char === "'" && !inDoubleQuote) { |
| 2143 | inSingleQuote = !inSingleQuote |
| 2144 | continue |
| 2145 | } |
| 2146 | |
| 2147 | if (char === '"' && !inSingleQuote) { |
| 2148 | inDoubleQuote = !inDoubleQuote |
| 2149 | continue |
| 2150 | } |
| 2151 | |
| 2152 | // A newline inside quotes: the NEXT line (from bash's perspective) starts |
| 2153 | // inside a quoted string. Check if that line would be stripped by |
| 2154 | // stripCommentLines — i.e., after trim(), does it start with `#`? |
| 2155 | // This exactly mirrors: lines.filter(l => !l.trim().startsWith('#')) |
| 2156 | if (char === '\n' && (inSingleQuote || inDoubleQuote)) { |
| 2157 | const lineStart = i + 1 |
| 2158 | const nextNewline = originalCommand.indexOf('\n', lineStart) |
| 2159 | const lineEnd = nextNewline === -1 ? originalCommand.length : nextNewline |
| 2160 | const nextLine = originalCommand.slice(lineStart, lineEnd) |
| 2161 | if (nextLine.trim().startsWith('#')) { |
| 2162 | logEvent('tengu_bash_security_check_triggered', { |
| 2163 | checkId: BASH_SECURITY_CHECK_IDS.QUOTED_NEWLINE, |
| 2164 | }) |
| 2165 | return { |
| 2166 | behavior: 'ask', |
nothing calls this directly
no test coverage detected