MCPcopy Create free account
hub / github.com/codeaashu/claude-code / isSafeHeredoc

Function isSafeHeredoc

src/tools/BashTool/bashSecurity.ts:317–514  ·  view source on GitHub ↗

* Checks if a command is a "safe" heredoc-in-substitution pattern that can * bypass the generic $() validator. * * This is an EARLY-ALLOW path: returning `true` causes bashCommandIsSafe to * return `passthrough`, bypassing ALL subsequent validators. Given this * authority, the check must be PRO

(command: string)

Source from the content-addressed store, hash-verified

315 * precisely replicate bash's heredoc-closing behavior.
316 */
317function isSafeHeredoc(command: string): boolean {
318 if (!HEREDOC_IN_SUBSTITUTION.test(command)) return false
319
320 // SECURITY: Use [ \t] (not \s) between << and the delimiter. \s matches
321 // newlines, but bash requires the delimiter word on the same line as <<.
322 // Matching across newlines could accept malformed syntax that bash rejects.
323 // Handle quote variations: 'EOF', ''EOF'' (splitCommand may mangle quotes).
324 const heredocPattern =
325 /\$\(cat[ \t]*<<(-?)[ \t]*(?:'+([A-Za-z_]\w*)'+|\\([A-Za-z_]\w*))/g
326 let match
327 type HeredocMatch = {
328 start: number
329 operatorEnd: number
330 delimiter: string
331 isDash: boolean
332 }
333 const safeHeredocs: HeredocMatch[] = []
334
335 while ((match = heredocPattern.exec(command)) !== null) {
336 const delimiter = match[2] || match[3]
337 if (delimiter) {
338 safeHeredocs.push({
339 start: match.index,
340 operatorEnd: match.index + match[0].length,
341 delimiter,
342 isDash: match[1] === '-',
343 })
344 }
345 }
346
347 // If no safe heredoc patterns found, it's not safe
348 if (safeHeredocs.length === 0) return false
349
350 // SECURITY: For each heredoc, find the closing delimiter using LINE-BASED
351 // matching that exactly replicates bash's behavior. Bash closes a heredoc
352 // at the FIRST line that exactly matches the delimiter. Any subsequent
353 // occurrence of the delimiter is just content (or a new command). Regex
354 // [\s\S]*? can skip past the first delimiter to find a later `DELIM)`
355 // pattern, hiding injected commands between the two delimiters.
356 type VerifiedHeredoc = { start: number; end: number }
357 const verified: VerifiedHeredoc[] = []
358
359 for (const { start, operatorEnd, delimiter, isDash } of safeHeredocs) {
360 // The opening line must end immediately after the delimiter (only
361 // horizontal whitespace allowed before the newline). If there's other
362 // content (like `; rm -rf /`), this is not a simple safe heredoc.
363 const afterOperator = command.slice(operatorEnd)
364 const openLineEnd = afterOperator.indexOf('\n')
365 if (openLineEnd === -1) return false // No content at all
366 const openLineTail = afterOperator.slice(0, openLineEnd)
367 if (!/^[ \t]*$/.test(openLineTail)) return false // Extra content on open line
368
369 // Body starts after the newline
370 const bodyStart = operatorEnd + openLineEnd + 1
371 const body = command.slice(bodyStart)
372 const bodyLines = body.split('\n')
373
374 // Find the FIRST line that closes the heredoc. There are two valid forms:

Callers 1

Calls 2

pushMethod · 0.45

Tested by

no test coverage detected