Skip to content

fix(guardrails): block heredoc-opener stdout redirect in block-hook-bypass - #136

Merged
kyle-sexton merged 2 commits into
mainfrom
chore/harden-heredoc-redirect
Jul 13, 2026
Merged

fix(guardrails): block heredoc-opener stdout redirect in block-hook-bypass#136
kyle-sexton merged 2 commits into
mainfrom
chore/harden-heredoc-redirect

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What

block-hook-bypass's strip_literals truncated the heredoc opener line at << (line="${line%%<<*}"), so a stdout redirect carried on the same line — cat <<EOF > generated.txt — was dropped before the redirect scan, letting a real file-write bypass through (hook exited 0).

Fix: drop only the heredoc operator + delimiter token, preserving the text before << and any text after the delimiter, so a trailing > file still reaches _cat_redir / _echo_redir.

Tests

Added two contract cases:

  • cat <<EOF > real.txt (+ body + terminator) → exit 2 (blocked)
  • cat <<EOF | cat (heredoc, no redirect) → exit 0 (allowed)

Full suite green (34/34); shellcheck clean; claude plugin validate plugins/guardrails passes. Patched guardrails 0.3.10.3.2.

Scope

Only the redirect-after-heredoc-operator opener form. The documented friction-guard floor (explicit-fd redirects, combined &>, args-before-redirect, etc.) stays accepted per the issue.

Refs melodic-software/medley#1461


Note

Medium Risk
Security-sensitive PreToolUse hook logic changed to close a real bypass; scope is narrow with new contract tests, but incorrect stripping could still miss or over-block Bash commands.

Overview
Closes a Bash hook-bypass hole in strip_literals: truncating the heredoc opener at << dropped same-line stdout redirects (cat <<EOF > file), so cat-redirect detection never ran.

strip_literals now removes only the << operator and delimiter token and keeps any suffix on that line (e.g. > file). The heredoc delimiter regex stops at > so forms like cat <<EOF>file still expose the redirect to the scan. Guardrails plugin version 0.3.1 → 0.3.2.

Contract tests cover opener redirects (spaced, glued, tab-strip, quoted delimiter) and confirm plain heredocs without redirects stay allowed.

Reviewed by Cursor Bugbot for commit 9d871a8. Bugbot is set up for automated code reviews on this repo. Configure here.

…k-bypass

strip_literals truncated the opener line at '<<', dropping a trailing
stdout redirect (cat <<EOF > file) before the redirect scan — a real
file-write bypass leaked (exit 0). Preserve the text after the heredoc
delimiter token so the redirect still reaches _cat_redir/_echo_redir.

Regression tests: opener redirect blocks (exit 2); plain heredoc with no
redirect stays allowed (exit 0). Patch guardrails 0.3.1 -> 0.3.2.
@cursor

cursor Bot commented Jul 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_9ae6746f-af56-4baa-a1d7-b8262683a343)

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

PR Review

  • Gather context from PR description and changed files
  • Review block-hook-bypass.sh fix logic
  • Review test coverage in block-hook-bypass.test.sh
  • Review plugin.json version bump
  • Post findings

View job run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Cursor Bugbot did not complete successfully (check skipped; usage limit reached), so automated review is incomplete and human review is needed before approval. No reviewers were assigned — the only human contributor is the PR author.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@kyle-sexton kyle-sexton changed the title harden(guardrails): block heredoc-opener stdout redirect in block-hook-bypass fix(guardrails): block heredoc-opener stdout redirect in block-hook-bypass Jul 13, 2026
@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 50s —— View job


PR Review

  • Gather context from PR description and changed files
  • Review block-hook-bypass.sh fix logic
  • Review test coverage in block-hook-bypass.test.sh
  • Review plugin.json version bump
  • Post findings

Summary

The fix is correct and well-scoped. The bug was real: line="${line%%<<*}" stripped everything from the first << onward, so a trailing redirect like > real.txt on a heredoc opener line was silently discarded before the _cat_redir/_echo_redir scan ran. The one-line replacement is the right fix.


Fix logic — block-hook-bypass.sh L116

# Before
line="${line%%<<*}"

# After
line="${line%%<<*}${line#*"${BASH_REMATCH[0]}"}"

Trace through cat <<EOF > real.txt:

  • heredoc_start_re matches at <<EOF; BASH_REMATCH[0] = <<EOF
  • ${line%%<<*} = cat (prefix before <<)
  • ${line#*" <<EOF"} = > real.txt (suffix after the full match)
  • Result: cat > real.txt_cat_redir fires correctly → exit 2 ✓

Trace through cat <<EOF | cat:

  • Same prefix extraction yields cat | cat
  • No > in the result → no redirect pattern matches → exit 0 ✓

Quoting: "${line#*"${BASH_REMATCH[0]}"}" — Bash allows double-quoted subexpressions inside #/##/%/%% patterns, so the inner "${BASH_REMATCH[0]}" is treated as a literal (not a glob). This is valid Bash 4.0+ syntax, and the hook already requires Bash 5.0+ (EPOCHREALTIME), so no compatibility concern.

Variation correctness:

  • <<-EOF > file — the regex has <<-?, so BASH_REMATCH[0] includes the -; suffix extraction is still correct ✓
  • <<'EOF' > file — quoted delimiter; BASH_REMATCH[0] = <<'EOF'; suffix = > file; result blocked ✓
  • <<\EOF > file — backslash-quoted delimiter; similar to above ✓
  • echo <<EOF > file — result echo > file; _echo_redir + _echo_file_out both fire → exit 2 ✓

Tests — block-hook-bypass.test.sh L99–104

The two new cases directly exercise the exact regression:

Case Expected
cat <<EOF > real.txt + body + EOF exit 2 (blocked) ✓
cat <<EOF | cat + body + EOF exit 0 (allowed) ✓

Minor gap to be aware of (not blocking): echo <<EOF > file (the echo+heredoc+redirect path) isn't covered by an explicit test. Given that echo <<EOF > file is syntactically unusual in practice and the echo redirect path already has thorough coverage elsewhere in the suite, this is a low-priority omission — but worth a follow-up if this plugin gets more adversarial testing.


Version bump

0.3.10.3.2 is the correct semver for a patch bug fix. ✓


Verdict

LGTM. The fix is minimal, mechanically correct, well-commented, and regression-tested. No changes requested.

A redirect glued to the delimiter (cat <<EOF>real.txt, no space) was
swallowed — the greedy delimiter capture absorbed EOF>real.txt, so no >
survived for the redirect scan (leaked, exit 0). Exclude > (keep < excluded)
from the delimiter body so bash's own word-boundary at > is honored.

Adds glued-form, tab-strip (<<-EOF), and quoted-delimiter (<<'EOF') opener
redirect regressions (37 cases, 0 fail).
@cursor

cursor Bot commented Jul 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_2623cb5b-45b0-4d1b-81fc-a5e84447160c)

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 36s —— View job


PR Review

  • Inspect new commit (9d871a8) vs previous reviewed commit (00b59d9)
  • Review updated block-hook-bypass.sh fix logic
  • Review test coverage in block-hook-bypass.test.sh
  • Review plugin.json version bump
  • Post findings

Summary

This PR now spans two commits, each closing a distinct heredoc-redirect bypass variant. Both fixes are correct and well-targeted.

Commit Variant fixed
00b59d9 cat <<EOF > real.txt (spaced redirect after delimiter)
9d871a8 cat <<EOF>real.txt (redirect glued to delimiter, no space)

First commit (00b59d9) — spaced redirect

Already reviewed in depth previously. Fix: replace line="${line%%<<*}" with the prefix+suffix extraction that preserves text after BASH_REMATCH[0]. Correct. ✓


Second commit (9d871a8) — glued redirect

The bug: The old delimiter character class [^[:space:]<][^[:space:]]* excluded < from the first char only, but allowed > anywhere in the delimiter body. For cat <<EOF>real.txt, the regex greedily captured EOF>real.txt as the delimiter. This meant BASH_REMATCH[0] = <<EOF>real.txt, the suffix after the full match was empty, and the result was cat — no redirect survived. The hook exited 0 (bypass).

The fix: Change the character class to [^[:space:]<>]+, uniformly excluding > from every position in the delimiter token. This matches bash's own word-boundary rule: > always terminates a shell word.

Trace through cat <<EOF>real.txt:

  • New regex captures only EOF (stops at >)
  • BASH_REMATCH[0] = <<EOF
  • ${line%%<<*} = cat
  • ${line#*" <<EOF"} = >real.txt
  • Result: cat >real.txt_cat_redir fires → exit 2 ✓

Edge case verification:

  • cat <<-EOF > real.txt (tab-strip) — <<-? handles the -; EOF still captured cleanly → blocked ✓
  • cat <<'EOF' > real.txt (quoted delimiter) — 'EOF' matches [^[:space:]<>]+ (no </> in the chars ', E, O, F); after the quote-strip logic delim = "EOF"; suffix > real.txt preserved → blocked ✓
  • cat <<EOF > real.txt (spaced, covered by first commit) — EOF still captured; suffix > real.txt preserved → blocked ✓
  • cat <<EOF | cat (no redirect) — result cat | cat, no > pattern → exit 0 ✓

> exclusion and quoted delimiters: A quoted delimiter like <<'EOF' cannot contain > without shell-level quoting errors, so [^[:space:]<>]+ is safe. The quote-stripping logic on lines 111-114 handles the wrapping quotes correctly after capture.


Tests

Three new cases added in 9d871a8 directly target the regression:

Case Expected
cat <<EOF>real.txt (glued) exit 2 ✓
cat <<-EOF > real.txt (tab-strip) exit 2 ✓
cat <<'EOF' > real.txt (quoted delimiter) exit 2 ✓

37 total cases, 0 failures per commit message. Suite now covers the full matrix of spaced/glued/tab-strip/quoted opener redirect forms.

Still untested (non-blocking, same note as before): echo <<EOF>file — the echo+heredoc+glued-redirect path. Low-priority given echo already has good redirect coverage elsewhere.


Version bump

0.3.10.3.2 is the correct semver increment for a patch fix. ✓


Verdict

LGTM. Both commits are minimal, mechanically correct, and well-tested. The glued-redirect variant (<<EOF>file) closes the last remaining bypass that the spaced-redirect fix left open. No changes requested.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: medium. Cursor Bugbot did not complete successfully (check skipped), so automated review is incomplete and human review is needed before approval. No reviewers were assigned because the only human contributor is the PR author.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@kyle-sexton kyle-sexton reopened this Jul 13, 2026
@cursor

cursor Bot commented Jul 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_22d9685e-5054-451e-b0f8-3ec55a9dbafc)

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 2s —— View job


PR Review

  • Gather context from PR description and changed files
  • Review block-hook-bypass.sh fix logic
  • Review test coverage in block-hook-bypass.test.sh
  • Review plugin.json version bump
  • Post findings

Summary

Two related but distinct heredoc-redirect bypass paths are closed in this PR. Both fixes are correct, minimal, and directly targeted at the real bypass vectors. No issues requiring changes.


Fix 1 — Delimiter regex (block-hook-bypass.sh L93)

local heredoc_start_re='(^|[^<])<<-?[[:space:]]*([^[:space:]<>]+)'

The old delimiter character class allowed > inside a delimiter token after the first character. For cat <<EOF>real.txt, the old regex greedily captured EOF>real.txt as the delimiter — making BASH_REMATCH[0] = <<EOF>real.txt, leaving no suffix for redirect detection, and the hook exited 0 (bypass).

The new [^[:space:]<>]+ uniformly excludes > from every position. This mirrors bash's actual word-boundary rule: > always ends a shell word, so it can never be part of a real heredoc delimiter. The + (vs the old two-part first-char class + tail class) is safe because a heredoc delimiter must have at least one character.

Trace through cat <<EOF>real.txt (glued bypass):

  • New regex matches <<EOF; BASH_REMATCH[0] = <<EOF
  • ${line%%<<*} = cat
  • ${line#*" <<EOF"} = >real.txt
  • Result: cat >real.txt_cat_redir fires → exit 2 ✓

Fix 2 — Opener line stripping (block-hook-bypass.sh L119)

# Before
line="${line%%<<*}"

# After
line="${line%%<<*}${line#*"${BASH_REMATCH[0]}"}"

The old single-parameter-expansion form truncated everything from the first << to end-of-line. Any suffix on the opener — including > real.txt — was silently discarded before the redirect scan ran.

The new form: prefix (everything before <<) concatenated with suffix (everything after BASH_REMATCH[0]). The inner "${BASH_REMATCH[0]}" is double-quoted inside the #*"..." pattern, so it is treated as a literal substring, not a glob — this is valid Bash 4.0+ syntax and safe since the hook already requires Bash 5.0+ (EPOCHREALTIME).

Trace through cat <<EOF > real.txt:

  • BASH_REMATCH[0] = <<EOF (group 1 matched the space before <<)
  • ${line%%<<*} = cat
  • ${line#*" <<EOF"} = > real.txt
  • Result: cat > real.txt_cat_redir fires → exit 2 ✓

Trace through cat <<EOF | cat (no redirect — control):

  • BASH_REMATCH[0] = <<EOF
  • Result: cat | cat
  • No > after cat[[:space:]]* → exit 0 ✓

Variation coverage:

Input form Result Verdict
cat <<-EOF > file cat > file blocked ✓
cat <<'EOF' > file cat > file blocked ✓
cat <<\EOF > file cat > file blocked ✓
cat <<EOF>file cat >file blocked ✓
cat <<EOF 2>err.txt cat 2>err.txt allowed ✓ (_cat_redir requires cat[[:space:]]*> — the 2 between spaces and > breaks the match)

Test coverage (block-hook-bypass.test.sh L100–116)

Five new cases cover the regression matrix:

Test case Expected Regression for
cat <<EOF > real.txt exit 2 spaced redirect on opener (Fix 2)
cat <<EOF | cat exit 0 control — no redirect
cat <<EOF>real.txt exit 2 glued redirect (Fix 1 + 2 combined)
cat <<-EOF > real.txt exit 2 tab-strip operator
cat <<'EOF' > real.txt exit 2 quoted delimiter

One non-blocking gap: echo <<EOF > file (echo + heredoc + redirect) has no explicit test. In practice, echo <<EOF is syntactically unusual and the echo-redirect path already has thorough standalone coverage. Low priority, but worth noting for adversarial-test completeness.


Version bump

0.3.10.3.2 is the correct semver increment for a patch bug fix. ✓


Verdict

LGTM. Both bypass paths are closed with the minimum necessary changes, the regex change correctly mirrors Bash's actual shell word boundary rules, the string extraction logic is mechanically sound, and the new tests directly exercise the regression cases. No changes requested.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: medium. Cursor Bugbot did not complete successfully (check skipped), so automated review is incomplete and human review is needed before approval. No reviewers were assigned because the only human contributor is the PR author.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@kyle-sexton
kyle-sexton merged commit fc1c9db into main Jul 13, 2026
42 of 51 checks passed
@kyle-sexton
kyle-sexton deleted the chore/harden-heredoc-redirect branch July 13, 2026 03:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant