Skip to content

Supply-chain risk: star-history workflow runs an unpinned third-party action with contents: write #55

Description

@Chirag6722

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

  1. 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.
  2. 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.
  3. 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.5a68d8f9d67ca20d55b682a264e69152dcf326e9c), 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions