Problem
.github/workflows/star-history.yml runs a third-party GitHub Action from a mutable branch reference while holding a contents: write token on the default branch:
permissions:
contents: write # granted to every step in the job
jobs:
star-history:
steps:
- uses: actions/checkout@v4
- uses: narayann7/star-history-action@main # <-- mutable ref
@main is not a fixed version. Every scheduled run (daily, cron: "17 0 * * *") re-resolves that ref and executes whatever code is on that branch at that moment. The action is a composite action, so its steps run directly in this repository's runner: it does npm ci inside the action's own renderer/ directory and then executes render-charts.sh and readme-embed.py from the action repo — all with GITHUB_TOKEN carrying contents: write and with the checked-out repo (and its .git credentials) on disk.
Impact
If the upstream repository is ever compromised, or if the maintainer force-pushes/changes main, the next scheduled run of this workflow executes attacker-controlled code with permission to push to main of a repository with 4k+ stars. The blast radius is:
- arbitrary commits pushed to
main (the workflow already pushes, so a malicious push looks routine — the last 8 runs all committed chore: update star history [skip ci]);
- exfiltration of the job's
GITHUB_TOKEN;
- tampering with the hardware STEP/BOM files that users download and machine.
The upstream action is a small personal project (18 stars, most recent push today) with no release-based pinning in use here, so "trust the branch" is the only control currently in place. This is exactly the scenario GitHub's own hardening guide addresses: "Pin actions to a full length commit SHA" — a tag can be moved, a branch changes constantly, only a SHA is immutable.
Secondary problems in the same file
- Workflow-wide
contents: write. The permission is declared at workflow level, so it applies to every job that may be added later. It should be scoped to the job that actually pushes.
- Redundant manual commit/push step.
star-history-action already commits and pushes by default (commit: true), and its own push does git pull --rebase first. The extra step in this workflow:
git add assets/star-history/
git commit -m "Update star history chart" || exit 0
git push
duplicates that logic without the rebase, so if anything lands on main between checkout and push, the run fails with a non-fast-forward error. In practice it is a no-op today (the action has already committed, so git commit finds nothing staged and || exit 0 swallows it), which means it is dead code that can only ever hurt.
- No concurrency guard. A manual
workflow_dispatch overlapping the scheduled run gives two jobs pushing to main at once.
Proposed solution
- Pin
narayann7/star-history-action to the full commit SHA of a released tag (v1.0.5 → a68d8f9d67ca20d55b682a264e69152dcf326e9c), with the version in a trailing comment so Dependabot can bump it.
- Pin
actions/checkout to the SHA for v5.0.0 (08c6903cd8c0fde910a37f88322edcfb5dd907a8) for the same reason and to move off the older v4.
- Set
permissions: contents: read at workflow level and grant contents: write only on the job that pushes.
- Delete the redundant manual commit/push step and let the action's rebase-aware push do the work.
- Add
concurrency: { group: star-history, cancel-in-progress: false }.
- Add
.github/dependabot.yml for the github-actions ecosystem so pinned SHAs get updated automatically instead of going stale — this is what makes SHA pinning maintainable rather than a one-off.
Happy to send the PR.
Problem
.github/workflows/star-history.ymlruns a third-party GitHub Action from a mutable branch reference while holding acontents: writetoken on the default branch:@mainis not a fixed version. Every scheduled run (daily,cron: "17 0 * * *") re-resolves that ref and executes whatever code is on that branch at that moment. The action is a composite action, so its steps run directly in this repository's runner: it doesnpm ciinside the action's ownrenderer/directory and then executesrender-charts.shandreadme-embed.pyfrom the action repo — all withGITHUB_TOKENcarryingcontents: writeand with the checked-out repo (and its.gitcredentials) on disk.Impact
If the upstream repository is ever compromised, or if the maintainer force-pushes/changes
main, the next scheduled run of this workflow executes attacker-controlled code with permission to push tomainof a repository with 4k+ stars. The blast radius is:main(the workflow already pushes, so a malicious push looks routine — the last 8 runs all committedchore: update star history [skip ci]);GITHUB_TOKEN;The upstream action is a small personal project (18 stars, most recent push today) with no release-based pinning in use here, so "trust the branch" is the only control currently in place. This is exactly the scenario GitHub's own hardening guide addresses: "Pin actions to a full length commit SHA" — a tag can be moved, a branch changes constantly, only a SHA is immutable.
Secondary problems in the same file
contents: write. The permission is declared at workflow level, so it applies to every job that may be added later. It should be scoped to the job that actually pushes.star-history-actionalready commits and pushes by default (commit: true), and its own push doesgit pull --rebasefirst. The extra step in this workflow:mainbetween checkout and push, the run fails with a non-fast-forward error. In practice it is a no-op today (the action has already committed, sogit commitfinds nothing staged and|| exit 0swallows it), which means it is dead code that can only ever hurt.workflow_dispatchoverlapping the scheduled run gives two jobs pushing tomainat once.Proposed solution
narayann7/star-history-actionto the full commit SHA of a released tag (v1.0.5→a68d8f9d67ca20d55b682a264e69152dcf326e9c), with the version in a trailing comment so Dependabot can bump it.actions/checkoutto the SHA forv5.0.0(08c6903cd8c0fde910a37f88322edcfb5dd907a8) for the same reason and to move off the older v4.permissions: contents: readat workflow level and grantcontents: writeonly on the job that pushes.concurrency: { group: star-history, cancel-in-progress: false }..github/dependabot.ymlfor thegithub-actionsecosystem so pinned SHAs get updated automatically instead of going stale — this is what makes SHA pinning maintainable rather than a one-off.Happy to send the PR.