fix(ci): a PR-authored test id cannot speak in the nightly bot's voice (#2585) - #2607
Conversation
#2585) Both nightlies splice a downloaded diff artifact into a sticky comment authored by `github-actions[bot]`. The artifact is produced by pytest over the MERGED PR TREE, so a `@pytest.mark.parametrize` value — arbitrary text chosen by whoever opened the PR, on a public repo that evaluates fork PRs — reaches the comment unfenced. The impact is not RCE on the runner; it is that the bot's comment can be made to say something its author never wrote, and the bot's voice is the only reason anyone trusts it. WHERE THE FIX WENT, AND WHY NOT WHERE THE ISSUE SAID. The issue asks for a fence around the spliced text in each workflow. Reading the producer first changed the answer twice over: * the artifact is STRUCTURED markdown — headings, a per-XML totals table, bullet lists — so fencing it would turn the table into monospace text on every ordinary nightly, destroying the thing that makes the comment useful; * the PR-authored surface is narrower than reported. There is no assertion text in this document (`_render_summary` emits ids and counts, never a failure message), and the table's other column is `s.path.name`, i.e. the `junit-base-pr<N>-<seed>.xml` name the workflow itself templates. Only the node id is untrusted. So the neutralisation is at the node id, where the untrusted input actually enters, and BOTH nightlies inherit it because both run this one producer — `integration-nightly.yml:432` needed no separate patch. `_inline_code` needs two properties and the backtick one alone is not enough: open with one more backtick than the longest run inside (CommonMark closes a span on the first run of EQUAL length, so a hard-coded delimiter is escapable by any input containing one — the rule #2533 applied to fenced blocks, at the inline level), AND collapse newlines first, because a code span cannot contain a blank line and an id carrying `\n## ` breaks out of the bullet however the backticks are counted. Edge backticks are space-padded, which CommonMark strips back off, and the id is capped at 300 chars so a megabyte-long parametrize value cannot bloat the comment past GitHub's body limit. AC item 3, confirmed rather than assumed: `--randomly-seed=${...}` is a command a human is invited to paste, and the seeds come from the workflow constant `NIGHTLY_SEEDS: '12345 67890 99999'` through the build matrix — never PR-derived. Pinned by a test so a change making seeds dynamic fails here. Tests: the CommonMark property stated directly, the newline property, bounds, padding, and the control the AC asks for — an ordinary id still renders as a plain single-backtick span and the document still renders as a TABLE, not a code block. Mutation-tested, and that found a hole in my own guard: deleting the neutralising call left every CommonMark test green because they exercise the helper directly, so the property is now also asserted on the rendered document, driven end to end from JUnit XML. With the fix reverted, 4 tests fail. The issue cites `HEADW-011`; that anchor arrives with the unmerged #2533 PR, so the durable note went to `learnings.md` rather than inventing a requirement id. Related to #2585
|
Resolve by running |
|
Resolve by merging |
… append collision, both entries kept)
|
merge-train: ejected at the batch gate — own check. The |
The `lint (sys.modules pollution check)` job fails on this branch and passes on dev: the module-scoped `dpf` fixture assigned `sys.modules[...]` directly and popped it in a `finally`. `pytest.MonkeyPatch.context()` is the same mechanism as the `monkeypatch` fixture at a scope that fixture cannot reach, and it RESTORES rather than deletes — a bare pop would unbind the name even if something else had legitimately bound it first. Related to #2585 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bd71qsYbFodvofba8P69eP
…-diff # Conflicts: # docs/memory/learnings.md
|
merge-train (2026-09-09): pushed |
|
Fixed in
|
Summary
Both nightlies splice a downloaded diff artifact into a sticky comment authored by
github-actions[bot]. The artifact is produced by pytest over the merged PR tree, so a@pytest.mark.parametrizevalue — arbitrary text chosen by whoever opened the PR, on a public repo that evaluates fork PRs — reaches the comment unfenced.The impact is not RCE on the runner. It is that the bot's comment can be made to say something its author never wrote, and the bot's voice is the only reason anyone trusts it.
Where the fix went, and why not where the issue said
The issue asks for a fence around the spliced text in each workflow. Reading the producer first changed the answer twice over:
_render_summaryemits ids and counts, never a failure message), and the table's other column iss.path.name— thejunit-base-pr<N>-<seed>.xmlname the workflow itself templates. Only the node id is untrusted.So the neutralisation is at the node id, where the untrusted input actually enters. Both nightlies inherit it because both run this one producer —
integration-nightly.yml:432needed no separate patch.The two properties, because the backtick rule alone is not enough
\n##breaks out of the bullet however the backticks are counted.Plus: edge backticks are space-padded (CommonMark strips them back off), and the id is capped at 300 chars so a megabyte-long parametrize value cannot bloat the comment past GitHub's body limit.
AC item 3 — confirmed, not assumed
--randomly-seed=${regressed[0].seed}is a command a human is invited to paste. The seeds come from the workflow constantNIGHTLY_SEEDS: '12345 67890 99999'through the build matrix — never PR-derived, so there is no untrusted token in that line. Pinned by a test, so a change making seeds dynamic fails here.Tests, and a hole mutation testing found in my own guard
The CommonMark property stated directly, the newline property, bounds, padding, and the control the AC asks for: an ordinary id still renders as a plain single-backtick span, and the document still renders as a table, not a code block.
Mutation-testing then showed the security tests were bypassable: deleting the neutralising call from
_format_test_idleft every CommonMark test green, because they exercise the helper directly. A neutraliser nothing calls is not a fix, so the property is now also asserted on the rendered document, driven end to end from JUnit XML. With the fix reverted, 4 tests fail.scripts/ci/diff-pytest-failures.py --self-test→ 13 cases pass (was 11).Nightly-related suites: 69 passed.
Note on the doc anchor
The issue cites
HEADW-011; that anchor arrives with the unmerged #2533 PR (#2590), so the durable note went tolearnings.mdrather than inventing a requirement id.Fixes #2585
🤖 Generated with Claude Code
https://claude.ai/code/session_01CdxGmvuKuqaWJZ6pKUgaUJ