Skip to content

fix: a ~/require:/forbid: Bash rule no longer matches words inside a heredoc body (#432) - #433

Merged
fdaviddpt merged 3 commits into
mainfrom
fix/432
Sep 23, 2026
Merged

fdaviddpt merged 3 commits into
mainfrom
fix/432

Conversation

@fdaviddpt

Copy link
Copy Markdown
Contributor

Closes #432

A ~ regex rule (and require:/forbid: beside it) was tested against the whole Bash command text, heredoc body included. A heredoc body is a payload piped to whatever the operator line names, not a command -- a word inside it that a rule targets tripped the rule on data the call never ran, the same false-positive shape as #7 (a quoted argument mentioning a blocked verb), one syntax form over.

Fix

jit_strip_heredoc_body() (scripts/common.sh) removes every heredoc BODY line -- and its own closing delimiter line -- from the command text before the fold that feeds all three matchers (fold_full in pre-tool-hook.sh), leaving the heredoc operator line itself visible so a rule can still target whatever actually runs on that line. Guarded against a here-string (<<<word) being mistaken for a heredoc.

Extended to require: and forbid:, not only the ~ arm the issue names: all three read the same fold_full, and satisfying a require: flag via text that only exists inside a heredoc payload (never a real argument) was itself a latent bypass of the same class #432 reports for forbid:. This closes it symmetrically instead of leaving one side open. Covered by a dedicated require: test row: a real argument still satisfies (not blocked); a heredoc-only mention no longer satisfies (now correctly blocked, "Missing required").

Self-review hardening (beyond the issue's own repro)

  • Lookahead, not a single-pass state machine. The first cut set in_heredoc on any <<WORD-shaped line and cleared it only on a later line equal to WORD -- so a false trigger (the shape appearing inside an ordinary quoted string, or a genuinely unclosed heredoc) left in_heredoc set for the rest of the string, silently dropping every command word after it from every rule. Rewrote to scan ahead for a genuine closing delimiter line before stripping anything; no such line anywhere in the rest of the command means nothing is stripped at all, so a false trigger can now only leave MORE of the command visible than a hand parser would, never less.
  • <<\DELIM (backslash-escaped, unquoted delimiter) is standard bash syntax and was not recognized by the operator regex (only single/double-quote-prefixed forms were). Extended the quote-class to include a backslash.
  • A heredoc piped to a genuine interpreter (bash <<EOF, sh, ssh host, python3, etc.) had its body stripped unconditionally by the first cut -- but that body IS the command that executes, not a payload, so a forbid: rm -rf row that correctly blocked a bare rm -rf / would have silently let it through inside bash <<EOF ... EOF. This is a new bypass this fix would otherwise have introduced. Added jit_heredoc_targets_interpreter(), a denylist of common interpreters/remote-exec commands checked as whole tokens against the operator line; a heredoc targeting one of those is left unstripped. A denylist, not an oracle, same posture this codebase already states out loud for its index-write guard -- a wrapper script the list has never heard of still gets its heredoc body stripped, which is A ~ Bash rule matches words inside a heredoc body, so a payload that merely mentions a blocked command is refused #432's own residual, not a regression this fix introduces.
  • CRLF-authored heredoc closing lines. The original rest == delim comparison had no CR-handling, so a CRLF-authored heredoc's real closing line (EOF\r) never matched an LF-only delim (EOF), which is the same silent-swallow-everything-after failure as the lookahead finding above, for a Windows-originated data shape. Fixed by the same lookahead: the candidate closing line is CR-trimmed before the delimiter comparison, and independently, the "no genuine close found = strip nothing" invariant means even an untrimmed CR mismatch would fail safe. Not separately covered by a literal-CR-byte test in tests/test-pre-tool-hook.sh (the existing CRLF section there tests rule-FILE content, not command-text CRLF) -- this is a stated coverage gap, defended by code-reading and by the shared lookahead invariant rather than by a dedicated test, left for follow-up given the practical difficulty of embedding a raw CR byte through the JSON/bash/run_hook test-fixture chain.

Known limitation (below-bar, not a regression)

Tests

SECTION 4b in tests/test-pre-tool-hook.sh: 340/343 before wiring jit_strip_heredoc_body() into fold_full (red, the 3 new assertions), 343/343 after (green). Self-review hardening added 6 more assertions: 347/353 red before, 353/353 green after (verified by stashing scripts/common.sh alone against the already-updated test file). Also green after the final commit: tests/test-security.sh (108/108), tests/test-commands.sh (21/21), tests/test-dogfood-entries.sh (97/97), tests/test-line-citations.sh (6/6). The repository's full bash tests/run-all.sh was not run by this lane; CI is the authority for that per this repo's own convention.

Docs

README.md (the repo's only docs_targets entry) does not document tool-rule matching internals at this level of detail (grepped for heredoc/multi-line/whole-command/full-command/regex-rule -- zero hits); no change needed.

changelog.d/432.fixed.md added; python3 .oss/assemble_changelog.py --check passes.

[AI-generated]

Florian DAVID added 3 commits September 23, 2026 14:46
…ody text (#432)

A tools/00-manual/*.md rule tested `fold_full` -- the whole raw command
text, heredoc body included -- against a `~` regex, `require:`, and
`forbid:` alike. A heredoc body is a payload piped to whatever the
operator line names, not a command, so a word inside it that a rule
targets refused a call the word never ran in -- the #7 false-block
shape one syntax form over.

jit_strip_heredoc_body() (scripts/common.sh) removes every heredoc BODY
line -- and its own closing delimiter line -- from the command text
before the fold, leaving the heredoc OPERATOR line itself untouched so
a rule can still target the command that actually runs on it. Guarded
against a here-string (<<<word), which opens no body and must not be
mistaken for one.

Co-Authored-By: Max <noreply>
Two independent reviewers found three real edges in the first cut of
jit_strip_heredoc_body():

- a `<<WORD`-shaped line that never opens a genuine heredoc (an
  ordinary quoted string containing the shape) left `in_heredoc` set
  for the rest of the command, silently dropping every real command
  word after it from `fold_full` -- worse than #432 itself, since
  nothing blocks and nothing says why. Fixed by requiring a genuine
  closing delimiter, found by scanning ahead, before anything strips;
  the same lookahead also closes a CRLF-authored heredoc whose real
  closing line never matched an LF-only delimiter.
- `<<\DELIM` (backslash-quoted, unquoted delimiter) was not
  recognized at all, so the original #432 bug persisted for this
  spelling. Recognized now, alongside the single/double-quoted forms.
- a heredoc piped to a known interpreter (bash/sh/ssh/python3/...) had
  its body stripped unconditionally, which would let a forbidden word
  slip past a forbid:/~/block row inside `bash <<EOF ... EOF` that
  correctly blocks the same word as a bare command -- the body IS the
  command there, not a payload. jit_heredoc_targets_interpreter() is a
  denylist of the concretely-reported cases; that heredoc is left
  alone.

Six new test pairs cover all three, red confirmed against the
previous commit and green against this one.

Co-Authored-By: Max <noreply>
Logged for the curation pass -- see the fragment for the reproduction
and the fix that was actually applied in scripts/common.sh.

Co-Authored-By: Max <noreply>
@fdaviddpt
fdaviddpt merged commit 7d247b1 into main Sep 23, 2026
13 checks passed
@fdaviddpt
fdaviddpt deleted the fix/432 branch September 23, 2026 16:03
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.

A ~ Bash rule matches words inside a heredoc body, so a payload that merely mentions a blocked command is refused

1 participant