(command: string)
| 225 | * @returns true if command substitution would be executed by bash |
| 226 | */ |
| 227 | export function detectCommandSubstitution(command: string): boolean { |
| 228 | let inSingleQuotes = false; |
| 229 | let inDoubleQuotes = false; |
| 230 | let inBackticks = false; |
| 231 | let i = 0; |
| 232 | |
| 233 | while (i < command.length) { |
| 234 | const char = command[i]; |
| 235 | const nextChar = command[i + 1]; |
| 236 | |
| 237 | // Handle escaping - only works outside single quotes |
| 238 | if (char === '\\' && !inSingleQuotes) { |
| 239 | i += 2; // Skip the escaped character |
| 240 | continue; |
| 241 | } |
| 242 | |
| 243 | // Handle quote state changes |
| 244 | if (char === "'" && !inDoubleQuotes && !inBackticks) { |
| 245 | inSingleQuotes = !inSingleQuotes; |
| 246 | } else if (char === '"' && !inSingleQuotes && !inBackticks) { |
| 247 | inDoubleQuotes = !inDoubleQuotes; |
| 248 | } else if (char === '`' && !inSingleQuotes) { |
| 249 | // Backticks work outside single quotes (including in double quotes) |
| 250 | inBackticks = !inBackticks; |
| 251 | } |
| 252 | |
| 253 | // Check for command substitution patterns that would be executed |
| 254 | if (!inSingleQuotes) { |
| 255 | // $(...) command substitution - works in double quotes and unquoted |
| 256 | if (char === '$' && nextChar === '(') { |
| 257 | return true; |
| 258 | } |
| 259 | |
| 260 | // <(...) process substitution - works unquoted only (not in double quotes) |
| 261 | if (char === '<' && nextChar === '(' && !inDoubleQuotes && !inBackticks) { |
| 262 | return true; |
| 263 | } |
| 264 | |
| 265 | // Backtick command substitution - check for opening backtick |
| 266 | // (We track the state above, so this catches the start of backtick substitution) |
| 267 | if (char === '`' && !inBackticks) { |
| 268 | return true; |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | i++; |
| 273 | } |
| 274 | |
| 275 | return false; |
| 276 | } |
| 277 | |
| 278 | /** |
| 279 | * Checks a shell command against security policies and allowlists. |
no outgoing calls
no test coverage detected