#20177: fix(security): block command substitution in unquoted heredoc bodies
maintainer
size: S
Cluster:
OpenClaw Plugin Enhancements
## Summary
Fixes an allowlist bypass in the shell command analyzer where unquoted heredoc bodies were not scanned for command substitution tokens.
**Advisory:** https://github.com/openclaw/openclaw/security/advisories/GHSA-65rx-fvh6-r4h2
## Problem
`splitShellPipeline` in `src/infra/exec-approvals-analysis.ts` parses shell commands to enforce exec allowlists. When it encounters a heredoc (`<<EOF`), it correctly identifies the delimiter but then skips the entire heredoc body without checking for disallowed tokens.
In bash, when the heredoc delimiter is **unquoted**, the shell performs command substitution on the body. This means:
```bash
cat <<EOF
$(id)
EOF
```
The analyzer sees `cat <<EOF`, checks that `cat` is allowlisted, and approves the command. But `bash -c` then executes `$(id)` inside the heredoc body - bypassing the allowlist entirely.
This is exploitable in unattended gateway deployments where the allowlist is the primary security boundary and there is no human to review non-allowlisted commands.
## Fix
- `parseHeredocDelimiter` now returns whether the delimiter was `quoted` (single or double quoted) or unquoted
- `HeredocSpec` stores the `quoted` state
- When scanning an unquoted heredoc body, the parser now rejects commands containing `` ` ``, `$(`, or `${` tokens
- Quoted heredocs (`<<'EOF'` / `<<"EOF"`) are unaffected - the shell treats their body as literal text, so no expansion occurs
## Test plan
- [x] Rejects `$(cmd)` in unquoted heredoc body
- [x] Rejects backtick substitution in unquoted heredoc body
- [x] Rejects `${VAR}` expansion in unquoted heredoc body
- [x] Rejects nested command substitution in unquoted heredoc
- [x] Allows `$(cmd)` in single-quoted heredoc body (safe - shell treats as literal)
- [x] Allows `$(cmd)` in double-quoted heredoc body (safe - shell treats as literal)
- [x] Allows plain text in unquoted heredoc body
- [x] Existing heredoc tests still pass
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Closes a real security hole in the shell command analyzer's heredoc handling. When a heredoc uses an **unquoted** delimiter (`<<EOF`), bash performs command substitution on the body — but the parser was skipping the body entirely, allowing `$(cmd)`, backticks, and `${...}` to sneak past the allowlist. The fix adds a `quoted` flag to `HeredocSpec` and `parseHeredocDelimiter`, then rejects unquoted heredoc bodies containing expansion tokens (`$(`/`${`/backtick). Quoted heredocs (`<<'EOF'`/`<<"EOF"`) are correctly left alone since the shell treats their bodies as literal.
- **Security**: Blocks the allowlist bypass described in GHSA-65rx-fvh6-r4h2
- **Test coverage**: 7 new tests covering `$(cmd)`, backticks, `${VAR}`, nested substitution, quoted heredocs, and plain text — all well-targeted
- **Minor test gap**: No test for `<<-` (strip-tabs) combined with an unquoted delimiter containing command substitution; the code handles it correctly, but explicit coverage would strengthen confidence
<h3>Confidence Score: 4/5</h3>
- This PR is safe to merge — it correctly closes a real allowlist bypass with a well-scoped, minimal fix.
- The fix is correct, minimal, and well-tested. The `quoted` flag accurately distinguishes safe (quoted) from unsafe (unquoted) heredocs, and the token scanning in the body loop catches the dangerous patterns. Score is 4 instead of 5 only because there's a minor test gap (no `<<-` + unquoted + substitution test) and no coverage for escaped expansion (`\$(cmd)`) which would be a false positive rather than a security gap.
- No files require special attention — changes are minimal and well-contained.
<sub>Last reviewed commit: a54bba6</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#16525: fix(shell): stop rejecting newlines in double-quoted args (#16470)
by yinghaosang · 2026-02-14
79.8%
#11961: fix: exec tool wraps shebang scripts in heredoc to use correct inte...
by scott-memco · 2026-02-08
77.2%
#20640: fix: prevent zsh glob expansion errors in exec commands
by okuyam2y · 2026-02-19
72.8%
#16907: fix(security): detect obfuscated commands that bypass allowlist fil...
by CornBrother0x · 2026-02-15
72.6%
#8161: fix(sandbox): block dangerous environment variables from Docker con...
by yubrew · 2026-02-03
72.2%
#9200: Fix: Strip dangerous env vars from baseEnv in host execution
by vishaltandale00 · 2026-02-05
71.7%
#20435: fix(exec): prioritize user 'always allow' config over tool defaults...
by ChisomUma · 2026-02-18
71.3%
#23654: security(cli): redact sensitive values in config get output
by SleuthCo · 2026-02-22
71.2%
#5496: Fix: Windows path separators stripped in Gateway scheduled task
by giuliozelante · 2026-01-31
71.0%
#18952: fix: sanitize schtasks env vars to prevent CRLF command injection
by coygeek · 2026-02-17
70.8%