Skip to content

fix(guardrails): strip_literals drops a quoted span's content unmarked, splicing a command word into a false echo #2385

Description

@kyle-sexton

Summary

strip_literals in plugins/guardrails/hooks/block-hook-bypass.sh drops a quoted span's content
without leaving any mark when the span is not a redirect operand. Where that span sits inside a
command word
, the two sides of the word are spliced together and can form a word bash never forms —
including the producer names _producer_head matches, so a command bash would run as ecxyho
blocks as an echo … > file write.

This is a pre-existing defect, not a regression: measured below, the single-line spelling has
been rc=2 since well before the change that surfaced it.

Evidence

Hook invoked as a decision function on PreToolUse Bash payloads (rc=2 blocked, rc=0 allowed).
base is 4c90b454 (guardrails 0.27.2); after is 0.28.0 (#2367, merged as eb9f3386):

                                                        base       after
ec"xy"ho hello > out.txt      (single line, non-empty)   rc=2       rc=2     <-- pre-existing
ec"x<NL>y"ho hello > out.txt  (multi-line,  non-empty)   rc=0       rc=2
ec""ho x > f                  (single line, empty span)  rc=2       rc=2
ec"<NL>"ho x > f              (multi-line,  empty span)  rc=0       rc=2

Bash's command word in each case:

command bash runs guard reads
ec"xy"ho hello > out.txt ecxyho echofalse positive
ec"x<NL>y"ho hello > out.txt ecx<newline>yho echofalse positive
ec""ho x > f echo echo — correct
ec"<NL>"ho x > f echo echo — correct

The discriminator is whether the dropped span was empty. An empty span really does vanish in
bash, so splicing is right; a non-empty one is literal content of the word, so splicing is wrong.
strip_literals does not distinguish them outside a redirect operand — inside one it already does,
via the \x03 OPAQUE mark (#2226 / #2287).

Where it comes from

The mark-based scheme from #2287 is gated on _in_redirect_operand, deliberately, so that
normalize_segments, _producer_head, _cat_redir and the whitespace trims stayed byte-for-byte
unchanged. Command-word spans were therefore left on the unmarked path. #2367 then made the
multi-line join empty (to stop a physical newline splitting a producer from its own redirect), which
brought the multi-line spelling into agreement with the single-line one — including on this defect.

Suggested direction, not prescribed

A coherent fix marks a non-empty dropped span as opaque wherever it occurs in a command word, for
both line shapes, so that:

  • ec"xy"ho hello > out.txt and ec"x<NL>y"ho hello > out.txt stop blocking (they are not echo);
  • ec""ho x > f and ec"<NL>"ho x > f keep blocking (they are echo);
  • printf 'a<NL>b' > notes.md keeps blocking — the span there is an argument, not part of the
    command word, and _producer_head matches printf before it.

Marking only the newline join, which is the narrower reading, would fix the multi-line half and put
the two spellings back in disagreement while losing the empty-span cases. That option should be
rejected explicitly rather than by omission.

Blast radius is strip_literals' unmarked-drop path, which every lane of this guard reads, so this
wants the same before/after direction count and floor set that #2287 and #2367 used.

Severity and provenance

Severity: LOW. It is a false positive (friction), not a bypass — the guard blocks something it
should allow, and the shape (ec"xy"ho) is structurally unusual. Filed because the guard's
producer-scoping is a stated contract and this is a real, measured departure from it.

Provenance: the symptom was raised by the automated reviewer on #2367
(#2367 (comment),
block-hook-bypass.sh:376), after that PR had merged. Its stated mechanism — "the empty join
reconstructs echo, so mark the newline opaque" — is incomplete: the four-row table above shows
the single-line form already did this on main, so the join is not the cause and marking it is not
the fix. Corrected mechanism recorded here so a future fix is not written against the wrong one.

Verified against 4c90b454 and eb9f3386.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions