Found by the security reviewer on hatlabs/halpi2#49.
The comment step sets GH_TOKEN: ${{ github.token }} on a step whose first command is uv run translation-status --comment. That entry point is resolved from the pull request head's own pyproject.toml and uv.lock — and uv sync --locked passes fine when a pull request edits both consistently, because it compares them against each other, not against the base. So the token sits in the environment of a process the pull request author chose.
Contained today, by GitHub's defaults rather than by anything we wrote: a pull_request from a fork gets a read-only GITHUB_TOKEN and no secrets, and a same-repo branch comes from someone who already has write access. It stops being contained if "Send write tokens to workflows from fork pull requests" is ever switched on. hatlabs/halpi2 and the hatlabs org both report can_approve_pull_request_reviews: true, so a write token there could submit approving reviews as github-actions[bot].
Fix: split the step in two. Generate comment.md with uv run translation-status --comment in a step with no GH_TOKEN, then post it from a second step that sets GH_TOKEN and only calls gh api. The token then never shares an environment with pull-request-controlled code, at the cost of one extra step.
Worth confirming separately that the fork write-token setting is off on halpi2, halmet, sh-esp32 and sh-rpi — it has no REST endpoint, so it needs eyeballing under Settings → Actions.
The same review noted what this workflow already gets right, so a later edit does not undo it: persist-credentials: false keeps the checkout credential out of .git/config while untrusted code runs; the comment lookup matches on author as well as marker; and ${{ }} values are passed through env rather than interpolated into run blocks.
Found by the security reviewer on hatlabs/halpi2#49.
The comment step sets
GH_TOKEN: ${{ github.token }}on a step whose first command isuv run translation-status --comment. That entry point is resolved from the pull request head's ownpyproject.tomlanduv.lock— anduv sync --lockedpasses fine when a pull request edits both consistently, because it compares them against each other, not against the base. So the token sits in the environment of a process the pull request author chose.Contained today, by GitHub's defaults rather than by anything we wrote: a
pull_requestfrom a fork gets a read-onlyGITHUB_TOKENand no secrets, and a same-repo branch comes from someone who already has write access. It stops being contained if "Send write tokens to workflows from fork pull requests" is ever switched on.hatlabs/halpi2and thehatlabsorg both reportcan_approve_pull_request_reviews: true, so a write token there could submit approving reviews asgithub-actions[bot].Fix: split the step in two. Generate
comment.mdwithuv run translation-status --commentin a step with noGH_TOKEN, then post it from a second step that setsGH_TOKENand only callsgh api. The token then never shares an environment with pull-request-controlled code, at the cost of one extra step.Worth confirming separately that the fork write-token setting is off on halpi2, halmet, sh-esp32 and sh-rpi — it has no REST endpoint, so it needs eyeballing under Settings → Actions.
The same review noted what this workflow already gets right, so a later edit does not undo it:
persist-credentials: falsekeeps the checkout credential out of.git/configwhile untrusted code runs; the comment lookup matches on author as well as marker; and${{ }}values are passed throughenvrather than interpolated intorunblocks.