Repository navigation
Stop the Doc Gates' Printed Names Forging Workflow Commands - #3116
Conversation
The Actions runner reads a line opening on `::` after leading whitespace is trimmed as a workflow command, and also a line holding `##[` anywhere, which is its legacy parser. A tracked name or file text shaped like either reached the output of both doc gates unchanged. repo_gate.py's printable() now spells the colon opening such a line and the bracket of each `##[` as escapes. prose_lint.py gains the same printable(), control escaping included, and applies it to every line carrying a path or file text, the finding line among them, which opened on the raw tracked name with no indent and printed a newline in a name raw.
The composite action echoed each line of the tracked .github/prose-gate-excludes raw, so an exclusion holding `##[` forged a legacy workflow command before prose_lint.py ran. The echo now escapes the bracket the way printable() does. The tests now cover the --summary and --list-files output and a name the scope note carries on stderr, and they share their case lists through tests/runner_commands.py. A parity test keeps the two gates' printable() copies equal, since each gate is fetched as one file.
The runner ends a line at a lone carriage return, and the trim keeps one inside an exclusion line, so an exclusion holding one could still open a workflow command on the line after it. The echo now spells it as \x0d.
|
A recorded local strict-review pass covers head |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
There was a problem hiding this comment.
🟡 Changes recommended
The scan-path summary still prints requested paths raw and can forge a legacy workflow command.
1 open finding
What changed in this PR
Hardens documentation gates against GitHub Actions workflow-command injection in paths, findings, file content, and exclusions.
Changes:
- Escapes current and legacy command markers in both gates.
- Sanitizes exclusion output in the composite action.
- Adds runner-command modeling and regression tests.
| File | Description |
|---|---|
tests/test_repo_gate.py |
Tests repository-gate output escaping. |
tests/test_prose_lint.py |
Tests prose-gate paths, findings, and exclusions. |
tests/runner_commands.py |
Models runner command parsing. |
.github/actions/repo-gate/repo_gate.py |
Escapes command-shaped output. |
.github/actions/prose-gate/prose_lint.py |
Escapes paths and file text. |
.github/actions/prose-gate/action.yml |
Escapes exclusion output. |
🧠 Review effort: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #3116 +/- ##
==========================================
Coverage ? 63.94%
==========================================
Files ? 17
Lines ? 9145
Branches ? 0
==========================================
Hits ? 5848
Misses ? 3297
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
… Fixes to Main (#3146) - #3135 makes `gh-write-guard` read a gh write's subcommand and target the way gh parses them: the subcommand is found past flags before the command and before the subcommand, an `R` inside a short-option cluster is read as a target, `GH_REPO` is read as a target, a run-time repository owner on a write fails closed, and a run-time word where `pr` or `merge` goes fails closed beside `--admin` or where word splitting may split it - #3139 releases the bootstrap lock when a `bootstrap.ps1` run ends rather than when its process does, so a second run typed at the same pwsh prompt is no longer refused the lock - #3130 matches tool names and the `all` keyword by case in the Windows agents loaders, as the Linux loaders already do - #3128 reads CodeRabbit's rate limit on a reply to an explicit review command in `pr_review.py` - #3126 reconciles VSCode-Server-DotNetCore in the hub after its resync - #3124 logs the Docker task's build-base leg in to Docker Hub - #3122 names the carried skill and the skipped review correctly - #3120 states the subprocess encoding rule in `CODESTYLE.md`, where carriers read Python style - #3118 and #3111 move the release-guard tests' workflow-command checks onto `runner_commands`, and test the alternatives wiring at the `audit_repo` and `fidelity_pass` call sites - #3116 stops the doc gates' printed names forging workflow commands - #3113 adds a catalog snippet for the release-with-smoke caller - #3109 moves the `workflow_reuse` fetch onto `audit.contents_text` - #3105 accepts `.codecov.yml` as an alternative to `codecov.yml` - #3103 adds a pip ecosystem to the carried Dependabot snippet - #3100 prefers the venv's ruff and pyright for a declared pip directory - #3098 tracks composite action pins in Dependabot - #3096 reads an uppercase Markdown suffix as Markdown at every prose gate branch After this merges, the host-safety installer payload changes, so the maintainer runs the installer after promotion. Closes #1416 Closes #1425 Closes #1578 Closes #1874 Closes #1892 Closes #1899 Closes #2804 Closes #2850 Closes #2852 Closes #2854 Closes #2996 Closes #3065 Closes #3067 Closes #3069 Closes #3079 Closes #3080 Closes #3089 Closes #3093 Closes #3101 Closes #3106 Closes #3107 Closes #3137 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a pull-request smoke build that runs when relevant release files change and reports the combined validation and build status. * Repository audits now recognize `.codecov.yml` as an alternative to `codecov.yml`. * **Improvements** * Python linting and type checks use tools from declared project environments when available, with fallbacks for missing tools. * Workflow and lint reports safely display unusual filenames and command-like text. * GitHub write-target checks now account for additional command forms and fail closed when targets cannot be verified. * Rate-limit detection now recognizes another CodeRabbit response format. <!-- end of auto-generated comment: release notes by coderabbit.ai -->

Summary
The Actions runner reads a stdout or stderr line as a workflow command in two ways, and a tracked name or a file's own text could reach either one through the output of both doc gates.
::once the runner trims leading whitespace, so the indent in front of arepo_gate.pyfinding gives no protection.##[anywhere in the line. The runner'sActionCommand.TryParsefinds it withIndexOfand still runs it when a registered command name follows up to]. The issue did not name this form, and it is fixed here as well.Changes
repo_gate.py'sprintable()now spells the colon opening such a line as\x3a, and the bracket of each##[as\x5b. That matches how it already spells each control character. Every finding and note passes through it, so no call site changed.prose_lint.pygains the sameprintable(), control escaping included, and applies it to every line carrying a path or file text. That covers the finding line,--list-files, the scope note, the summary's per-file lines, the filesystem-walk warning, and the error lines that quote paths or the diff base. The finding line opened on the raw tracked name with no indent, so a name opening on::forged a command there directly. A name holding a newline also printed raw, which let it start a line of its own..github/prose-gate-excludesraw, beforeprose_lint.pyran. A fork controls that file, so an exclusion holding##[, or a carriage return followed by::, forged a command there. The echo now spells the bracket as\x5band a carriage return as\x0d.tests/runner_commands.pymodels which lines the runner would pass to a command parser. It follows the runner's two parsers rather than the gates' own escaping. Both gates' tests use it, throughprintable()and through a real run over tracked names built to forge each form. The run covers the finding lines,--summary,--list-files, and a name the scope note prints on stderr. A parity test keeps the two gates'printable()copies equal, since each gate is fetched downstream as one file and cannot import the other.Design choice
The issue left a choice: neutralize the printed lines, or wrap the findings in a
::stop-commands::block with a random token. This change neutralizes the lines, for three reasons:##[form along with the::form, in a single helper each gate already routes its output through.stop-commandsblock depends on its resume line being printed. A gate killed mid-run would leave command processing off for the rest of the step, and that includes the composite action's own::error::lines.Verification
The new tests fail with the two neutralizing lines removed from both gates (14 failures). Removing a single call site also fails a test: the finding line's, the
--summaryline's, or the--list-filesline'sprintable(), or either substitution in the action's echo.python3 -m unittest discover -s testspasses with 2867 tests and 7 skipped. ruff format, ruff check, and mypy are clean, as areprose_lint.py --diff origin/develop,repo_gate.py, andspec/validate.py.actionlint, run through
scripts/docker_lint.py, is clean.The local strict review ran three rounds. Round 1 found the action's echo and the untested output modes. Round 2 found the carriage-return route. Round 3 raised no findings.
Follow-up
tests/runner_commands.py. That check misses a command line led by whitespace.Closes on promotion: #1874
🤖 Generated with Claude Code
Summary by CodeRabbit