Skip to content

Check GitHub Actions for known vulnerabilities #253

Description

@fniessink

The problem

Update-time pins a GitHub Action to a commit SHA because an action is a supply-chain risk, and then never asks whether the version it pinned is one a security advisory names. The README explains the silence under the per-type "Vulnerable dependencies" answer. OSV holds advisories for actions, it says, but "their affected entries enumerate no versions", so a query "comes back empty however that version is spelled", and "silence there says nothing about whether the action is safe".

The conclusion is right and the reason is wrong, and the difference is what makes a check possible. OSV holds advisories for actions, and those advisories do carry versions (probed 2026-08-15 against https://api.osv.dev/v1/query with ecosystem GitHub Actions):

Action Advisory What the advisory says is affected
tj-actions/changed-files GHSA-mrrh-fwg8-r2c3 range, introduced: 0fixed: 46.0.1
tj-actions/changed-files GHSA-mcph-m25j-8j63 range, introduced: 0fixed: 41
reviewdog/action-setup GHSA-qmg3-hpqr-gqvc versions: ['1']

What OSV declines to do is the matching. The same query carrying a version comes back empty, whatever the version is spelled like:

version '45.0.7': 0 advisories
version 'v45.0.7': 0 advisories
version '45': 0 advisories

So the advisory covering 45.0.7 sits in OSV's answer to a version-less query and is missing from its answer to a version-bearing one. Update-time asks the version-bearing question, receives nothing, and reports nothing. GHSA-mrrh-fwg8-r2c3 is the March 2025 tj-actions/changed-files compromise, in which the tags of released versions were repointed at a commit that wrote CI secrets into build logs — the attack that hash pinning exists to blunt, and the one whose advisory Update-time cannot currently see.

What the check would do

Ask OSV for the action's advisories without naming a version, then match the version the run leaves the reference on against each advisory's ranges and versions entries. Report a match exactly as a vulnerable Python dependency is reported today: at the reference's line, at the advisory's risk level, naming the advisory and its URL.

Scope

  • The references that name a version: an action referenced by a version tag (uses: actions/checkout@v4, @4.1.1) and one already pinned to a commit SHA carrying a version comment (@<sha> # v4.1.1). An action pinned to a bare SHA or to a branch names no version, so it stays unchecked.
  • The vulnerable scope of the # update-time: marker, which Update-time currently reports as redundant on an action, has to start holding the new warning back — in all its forms, so ignore[vulnerable], ignore[vulnerable=ID], and ignore[vulnerable<LEVEL].
  • --vulnerability-level and --ignore-vulnerability apply as they do for every other checked reference.

The design constraint

The version matching becomes Update-time's, since OSV will not do it. An advisory's ranges are ECOSYSTEM-typed, which means they are ordered by the ecosystem's own comparison, and an action's versions are git tags: sometimes v-prefixed, sometimes major-only, and fixed: 41 above is a major-only bound. Deciding that 45.0.7 falls below 46.0.1, and what a v4 pin means against a fixed: 41, needs a comparison that tolerates all of those shapes. Update-time already orders such tags when it picks the latest version to move an action to, so the comparison itself is not new; what is new is applying it to a bound the advisory states rather than to a tag the repository publishes.

Out of scope

  • Pre-commit hooks. A hook repository is not a package in OSV's GitHub Actions ecosystem unless that same repository also publishes an action, so the vulnerable scope stays redundant there.
  • Reading an advisory's remediation to force an update past the fix. The update path already moves a reference to the newest version its bounds allow.

Documentation

  • The per-type "Vulnerable dependencies" answer for GitHub Actions and pre-commit hooks, which currently says neither is checked and why.
  • The Python-dependency-style row for GitHub Actions in the dependency-type table under "Vulnerable dependencies".
  • The "Redundant markers" section, which names a vulnerable scope on a GitHub Action as reported redundant.
  • A changelog entry under [Unreleased].

Open questions

  1. Does Update-time match ECOSYSTEM ranges itself for actions alone, or does that matching move into the OSV source for every ecosystem, so a future ecosystem OSV cannot match is handled the same way?
  2. What is reported for a reference whose version names a line rather than a release — a @v4 tag, or a # v4 comment — when the advisory's bound is fixed: 4.1.2? Every 4.x below the fix is affected and every one above it is not, and the reference names neither.
  3. Do reusable workflows (uses: org/repo/.github/workflows/ci.yml@v1) get the same check, given that OSV would know them under the repository's name?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions