Skip to content

Escape repository-controlled values in text and Markdown output - #125

Merged
thossullivan merged 4 commits into
mainfrom
fix/escape-report-output
Oct 24, 2026
Merged

thossullivan merged 4 commits into
mainfrom
fix/escape-report-output

Conversation

@thossullivan

Copy link
Copy Markdown
Owner

Summary

  • alert --format markdown now renders file paths, matched strings, waiver owners and reasons, and channel names as code spans, so repository content can't add links, images, headings, <details>, or mentions to job summaries and PR comments.
  • check, inventory, and schedule text output and the stderr coverage notes now escape control characters, so a file name with a newline can't emit Actions workflow commands into the job log.
  • The bot's hidden metadata comment also encodes < and >, since HTML closes a comment at --!> as well as -->.
  • alert --format github, JSON outputs, CycloneDX, and the badge already escaped correctly and are unchanged.

Tests

  • 10 new assertions in test/run.mjs and 1 in bot/test/run.mjs. Each fails on the old code.
  • npm test passes.

Visible change: paths in Markdown alerts now render in code font.

🤖 Generated with Claude Code

alert --format markdown printed file paths, matched strings, and waiver
owners and reasons as raw Markdown, so a crafted file name or waiver could
inject links, images, headings, details blocks, or mentions into job
summaries and PR comments. The text formats (check, inventory, schedule)
and the stderr coverage notes passed control characters through, so a file
name containing a newline could emit Actions workflow commands such as
::warning:: into the job log.

Markdown alerts now render untrusted values as code spans whose fence
outruns any backtick run in the value. Text output and stderr notes run
those values through printable(). The GitHub annotation format keeps its
existing escaping. The bot's hidden metadata comment now also encodes < and
>, because HTML closes a comment at --!> as well as -->.

Implemented by an Opus subagent; reviewed by Claude.
Review of the first pass found the same bug class on paths it did not
touch: config and policy errors on stderr echoed .model-eol.json text,
apply printed plan file paths raw, bot issue evidence and stale-work
comments kept newlines, mentions, table pipes, and autolinks, U+2028 or
U+2029 in feed text broke the bot's metadata parse (duplicate issues and
permanent PR conflicts), and the runner's legacy ##[ command form matches
anywhere in a line.

printable() moves to lib/printable.mjs and also escapes bidi controls,
zero-width marks, and line separators, and breaks ##[. printableLines()
keeps intended line breaks in multi-line errors but stops any line from
starting a :: command. The bot's markdownText now produces one inline line
with @ and | encoded and bare autolinks broken; repository paths, matched
strings, and waiver text in bot bodies are code spans. metadataLine encodes
U+2028 and U+2029.

JSON outputs, GitHub annotations, and ordinary-input text are
byte-identical to main.

Implemented by an Opus subagent; reviewed by Claude.
GitHub resolves @mentions and #references after decoding HTML entities,
so the &#64; and &#35; encoding from the previous commit still produced
live mentions and issue links when rendered. markdownText now inserts a
zero-width space after @ and # (and in www. and ://) instead.

The Action prints JSON reports into the job log, and the runner's legacy
command form matches ##[ anywhere in a line, so a file named
"x ##[set-output ...]" reached the log through check --json, plan,
inventory, schedule, alert, and verify --json. Printed JSON now writes the
second hash as #, which JSON.parse reads back unchanged; objects,
digests, receipts, and files written by apply are untouched.

The bot's evaluate output, its feed-download and GitHub API warnings, and
markdownLink labels (which could show github.com while linking elsewhere)
now go through the same escaping.

Implemented by an Opus subagent; reviewed by Claude.
Rendering bot text through GitHub's Markdown API showed three gaps.
owner/repo/issues/N paths (with or without a scheme), /issues/N, and GH-N
still became issue links, which leave cross-references on the target
issue; markdownText now puts a zero-width space after every slash and
inside GH-N. GitHub detects www. on the raw text, so the zero-width space
moves before the dot. A value starting with -, +, =, leading spaces, or
N. could still open a rule, list, or code block inside a list item; the
leading marker is now encoded and leading spaces dropped.

Fifteen hostile and ordinary samples rendered through the API produce no
links, mentions, rules, or lists, and read unchanged.

Written by Claude after three implementer rounds.
@thossullivan
thossullivan force-pushed the fix/escape-report-output branch from b1b5be3 to 895cd98 Compare October 24, 2026 02:05
@thossullivan
thossullivan merged commit 0add0ff into main Oct 24, 2026
6 checks passed
thossullivan added a commit that referenced this pull request Oct 24, 2026
#125 escapes text output and stderr, JSON printed to logs, Markdown alerts,
and bot-written bodies, in addition to the annotations and badge JSON the
scope already listed.
thossullivan added a commit that referenced this pull request Oct 24, 2026
…130)

* Rewrite the docs for human readers and move agent rules to AGENTS.md

The README had grown to 714 lines mixing user docs, the spec pitch, and
refresh-pipeline internals, and much of the prose read as machine
written. It is now the user path (quick start, commands, Action, config,
how retirement dates work, the bot, data sources, limits), with reference
material split into docs/CONFIGURATION.md, docs/BOT.md, docs/RECEIPTS.md,
and docs/FEEDS.md, and parser behaviour in refresh/README.md.

docs/CONTEXT.md, docs/DESIGN_BOT.md, and docs/PRODUCT_PLAN.md are removed.
Their settled design decisions now live in CONTRIBUTING.md, the current
bot architecture in docs/BOT.md, and agent-only rules in AGENTS.md, which
CLAUDE.md imports.

Two fact-check passes against the code corrected claims that were wrong
before this rewrite too: waiver expiry does not undo a human dismissal,
earliest dates can fail CI, the bot opens PRs without a configured eval,
cloud and gateway references open issues under scope direct, routes do
not make a routed alias fail under scope direct, the refresh never drops
models, and amazon.json is carried forward. Stale Azure and Bedrock dates
and the demo GIF are refreshed. SPEC.md and SECURITY.md change wording
only, not requirements or scope.

The two tests that read README text now read docs/BOT.md and
docs/RECEIPTS.md.

Written by Claude; fact-checked and prose-reviewed by Opus subagents.

* Describe the merged parser and fetch behaviour in refresh/README.md

Updates the parser notes for #126 through #129: section-scoped earliest
precision and the retired-models archive rule, the stricter OpenAI skip
rule and the alias | snapshot family rule, the Bedrock regional legacy
rule, and bounded fetches with redirect refusal for keyed requests.

* Name every escaped output in the security scope

#125 escapes text output and stderr, JSON printed to logs, Markdown alerts,
and bot-written bodies, in addition to the annotations and badge JSON the
scope already listed.
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