Skip to content

ci: remove the !fix autofix workflow, enforce formatting in lint instead - #27

Merged
ryanio merged 4 commits into
mainfrom
security/harden-fix-lint-workflow
Aug 22, 2026
Merged

ci: remove the !fix autofix workflow, enforce formatting in lint instead#27
ryanio merged 4 commits into
mainfrom
security/harden-fix-lint-workflow

Conversation

@ryanio

@ryanio ryanio commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Removes the !fix autofix workflow and enforces formatting in CI instead.

Why remove rather than harden

It fired. On 2026-05-22 an external user with author_association: NONE commented !fix on a fork pull request whose head branch was named main. actions/checkout defaults repository to github.repository, so ref: main resolved against this repo rather than the fork, and the bot committed and pushed to the default branch:

Commit Author When
49e1a78 GitHub Actions Bot 2026-05-22T08:07:26Z
4cbaa91 GitHub Actions Bot 2026-05-22T08:07:26Z

Both are on main. Run 26276212320, event issue_comment, conclusion success. Not code execution, but an unauthenticated write to the default branch.

Hardening keeps the shape that made it possible: an issue_comment trigger, which runs in the base repo context with the repo's secrets, holding contents: write, checking out a pull request's code. An earlier revision of this PR added an author-association gate and a fork refusal, and that would have stopped the May incident. Deleting the workflow removes the surface instead of narrowing it, and the convenience it bought is one forge fmt away locally.

What replaces it

Nothing new. lint.yml already ran forge fmt --check; it now fails the build and says to run forge fmt, rather than advertising a bot. Two fixes went in alongside:

  • Dropped ref: ${{ github.head_ref }}. On a pull_request event, checkout defaults to the merge commit, which is what CI should judge. Naming head_ref resolves a bare branch name against this repository, which is the same resolution that made the May incident possible.
  • Added permissions: contents: read, and pinned actions/checkout and foundry-rs/foundry-toolchain by SHA.

Verified, and one thing I did not

The two bot commits, the run id, and the event type are read from the API, not inferred. lint.yml parses as valid YAML and the run block is four lines.

Not verified: whether any comment other than the two probes ever triggered the workflow. Run retention has expired for anything older, so how often it was legitimately used is unknown. If maintainers relied on it, the tradeoff here is worth saying out loud rather than assuming nobody will miss it.

Unrelated failure

Forge Tests fails on this branch and on main. foundry.toml remaps openzeppelin-contracts/ to lib/openzeppelin-contracts/contracts/ while lib/shipyard-core imports openzeppelin-contracts/contracts/utils/..., producing a doubled contracts/contracts/ path. This PR changes only workflow YAML and cannot affect Solidity compilation. Being fixed separately.

Still open for you

Whether to close the two probe PRs, #25 and #26. Untouched here.

Ryan Ghods and others added 3 commits August 22, 2026 13:42
…ranches

Autofix Linting triggers on issue_comment, which runs in the base repository
context with contents: write. It gated only on the comment body, so any
GitHub user could start it, and it resolved the pull request head branch by
name against this repository. On 2026-05-22 an outside account opened a fork
pull request whose head branch was named main, commented !fix, and the job
checked out this repository's main, ran forge fmt and pushed two commits to
it (49e1a78, 4cbaa91).

- gate on comment author_association so only OWNER, MEMBER or COLLABORATOR
  can trigger the job
- refuse fork pull requests and refuse the default branch
- write the branch through $GITHUB_OUTPUT. ::set-output still works on hosted
  runners, so this is hygiene rather than the bug
- state the whole if condition as one expression. The old form mixed a bare
  operand with ${{ ... }}, which compiles to a format() call whose non-empty
  string result is always truthy
- quote every expansion, pass event data through env, validate the branch name
- drop checks: write, default the workflow to no permissions, and scope
  contents: write plus pull-requests: read to the job
- upgrade and pin actions/checkout and foundry-rs/foundry-toolchain by SHA
- use printf so .git-blame-ignore-revs entries get a real newline

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r comment

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The autofix workflow was reachable by anyone who could comment. On 2026-05-22
an external user with author_association NONE commented "!fix" on a fork pull
request whose head branch was named "main"; checkout defaults `repository` to
the base repo, so `ref: main` resolved against shipyard and the bot committed
and pushed 49e1a78 and 4cbaa91 to this repository's default branch.

Hardening it means keeping an issue_comment trigger with contents: write that
checks out a pull request's code. Deleting it removes that surface entirely
and costs a convenience that "forge fmt" already provides locally.

Formatting is still enforced, by the lint workflow, which now fails the build
instead of pointing at the bot. That workflow also stops overriding the
checkout ref with github.head_ref, which is the same bare-branch-name
resolution that made the May incident possible, and now declares
permissions: contents: read.
@ryanio ryanio changed the title fix(ci): restrict the !fix autofix workflow to maintainers and repo branches ci: remove the !fix autofix workflow, enforce formatting in lint instead Aug 22, 2026
@ryanio
ryanio merged commit b6aced1 into main Aug 22, 2026
4 checks passed
@ryanio
ryanio deleted the security/harden-fix-lint-workflow branch August 22, 2026 21:07
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