Skip to content

[Security] medium: Third-party GitHub Actions pinned to mutable tags (not commit SHAs) in secret- and signing-privileged workflows #467

Description

@Kravalg

Summary

Every third-party action is referenced by a mutable ref (major-version tag, named release tag, or a Docker :latest tag) rather than a pinned commit SHA. The affected workflows are exactly the ones that receive the most sensitive secrets and privileges: code-lint-and-prettify.yml feeds the GitHub App private key (secrets.VILNACRM_APP_PRIVATE_KEY) to tibdex/github-app-token@v2 and the code-signing key (secrets.GPG_PRIVATE_KEY / GPG_PASSPHRASE) to crazy-max/ghaction-import-gpg@v6, then runs super-linter/super-linter@v7.1.0 with FIX_* autofix and pushes a GPG-signed commit back to the PR branch via stefanzweifel/git-auto-commit-action@v5. template-sync.yml and autorelease.yml hand the App token to AndreasAugustin/actions-template-sync@v2, TriPSs/conventional-changelog-action@v5 and actions/create-release@v1. graphql-diff.yml uses kamilkisiela/graphql-inspector@release-1701263349990 (a moveable release ref) and openapi-diff.yml uses uses: docker://openapitools/openapi-diff:latest (a mutable image tag pulled at runtime). Because tags/branches/:latest are re-pointable, any upstream repo takeover, tag-move, or malicious release of any of these actions executes attacker code inside a job holding the org's App private key, the GPG signing key, and contents:write.

Severity: MEDIUM • Category: CICD-SEC-4 Poisoned Pipeline Execution / CICD-SEC-3 Dependency Chain Abuse (unpinned actions)
Found by an authorized automated adversarial pentest loop and confirmed by 2 independent skeptic verifiers (unanimous).

Affected code / location

.github/workflows/code-lint-and-prettify.yml:19,34,91,112; template-sync.yml:14,25; autorelease.yml:15,26,32; graphql-diff.yml:60,67; openapi-diff.yml:50,70; codecov.yml:49

Exploit scenario

  1. Attacker compromises (or performs a tag/release re-point on) any one of the pinned-by-tag third-party actions above (e.g. stefanzweifel/git-auto-commit-action's v5 tag, or the openapitools/openapi-diff:latest image). 2. On the next PR/push/schedule, the runner resolves the mutable ref to the attacker's code. 3. In code-lint-and-prettify.yml the malicious step reads the already-imported GPG private key from the agent and $GPG_PASSPHRASE (written to GITHUB_ENV) plus steps.generate_token.outputs.token and exfiltrates them (e.g. curl to attacker host), or uses the in-job contents:write token to push a signed commit to a protected branch. 4. Result: theft of the VilnaCRM GitHub App private key (org-wide contents:write) and the commit-signing key, plus ability to push verified/signed malicious commits. No pinning + no allowed-actions policy means there is no second control.

Suggested remediation

Pin every third-party action to a full 40-char commit SHA (keep the human tag as a trailing comment), replace docker://openapitools/openapi-diff:latest with a digest-pinned image, enable Dependabot for github-actions to bump the SHAs, and turn on the repo/org 'Allow select actions' + 'require actions pinned to SHA' policy. Move secret-handling into a minimal, isolated job.

Verification notes

  • Verifier 1 (CONFIRMED, exploitable=true, duplicate=false): Verified all cited lines. Every third-party action is pinned to a mutable ref, not a commit SHA: code-lint-and-prettify.yml uses tibdex/github-app-token@v2 (line 19, receives secrets.VILNACRM_APP_PRIVATE_KEY line 22), crazy-max/ghaction-import-gpg@v6 (line 34, receives GPG_PRIVATE_KEY/GPG_PASSPHRASE lines 36-37), super-linter/super-linter@v7.1.0 (line 91, FIX_* autofix + generate_token output line 105), and stefanzweifel/git-auto-commit-action@v5 (line 112, pushes a GPG-signed commit). Critically GPG_PASSPHRASE is exported to GITHUB_ENV on line 50, so any subsequent step in that job (including a compromised action) can read it, plus the imported GPG secret key material and steps.generate_token.outputs.token. template-sync.yml pins tibdex/github-app-token@v2 (14) and AndreasAugustin/actions-template-sync@v2 (25) with the App token on a schedule. autorelease.yml pins tibdex/github-app-token@v1 (15), TriPSs/conventional-changelog-action@v5 (26), actions/create-release@v1 (32) with the App token on push-to-main. graphql-diff.yml pins kamilkisiela/graphql-inspector@release-1701263349990 (67, a moveable release ref) and EndBug/add-and-commit@v9 (60) in a job with contents:write. openapi-diff.yml uses docker://openapitools/openapi-diff:latest (70, mutable image tag pulled at runtime). codecov.yml pins codecov/codecov-action@v4 (49, receives CODECOV_TOKEN). No SHA pinning is present anywhere and there is no in-repo allowed-actions restriction acting as a second control, so nothing mitigates a mutable-ref resolution. The privileged secrets are genuinely reachable by the unpinned actions on internal-branch PRs, push-to-main, and scheduled runs (fork PRs are excluded from secrets but that does not close the path). This matches OWASP CICD-SEC-4/SEC-3 and GitHub's own hardening guidance to pin to full-length SHAs. It is materially different from known issues [Security] critical: OAuth2/JWT access tokens silently escalate to ROLE_SERVICE; issuer/audience validation skipped; session-revocation bypassed #312-[Security] low: Account-lockout failure counter uses non-atomic read-modify-write (race allows extra guesses) #324 (none address action pinning / supply-chain). The sole caveat: exploitation requires an external precondition (upstream action takeover, tag/release re-point, or malicious :latest push), which is inherent to this supply-chain finding class rather than a mitigation in the present code; the vulnerable configuration itself is present, reachable, and unguarded. Severity medium is appropriate given the sensitivity (org-wide App private key with contents:write plus commit-signing GPG key). Should reference NFR umbrella Enterprise readiness audit: close remaining gaps to reach 5/5 on all quality attributes and NFRs #348/[NFR] Accessibility — score 3/5: HTML-only emails, untranslated/typo'd confirmation template, no lang attributes, recipient locale not applied #349-441 while remaining its own actionable issue.
  • Verifier 2 (CONFIRMED, exploitable=true, duplicate=false): All cited references verified in the actual files. code-lint-and-prettify.yml pins tibdex/github-app-token@v2 (fed secrets.VILNACRM_APP_PRIVATE_KEY), crazy-max/ghaction-import-gpg@v6 (fed GPG_PRIVATE_KEY/GPG_PASSPHRASE), super-linter/super-linter@v7.1.0 (autofix + App token), and stefanzweifel/git-auto-commit-action@v5 (pushes GPG-signed commit) — all mutable tags. GPG_PASSPHRASE is also written to GITHUB_ENV, exposing it to any subsequent step in the job. template-sync.yml pins tibdex/github-app-token@v2 and AndreasAugustin/actions-template-sync@v2 (App token). autorelease.yml pins tibdex/github-app-token@v1, TriPSs/conventional-changelog-action@v5, actions/create-release@v1. graphql-diff.yml uses kamilkisiela/graphql-inspector@release-1701263349990 (moveable release ref) plus EndBug/add-and-commit@v9 with contents:write. openapi-diff.yml uses docker://openapitools/openapi-diff:latest (mutable image). codecov.yml uses codecov/codecov-action@v4 with CODECOV_TOKEN. There is no SHA pinning, no least-privilege permissions block on the secret-bearing lint job, and no allowed-actions policy — so no second control exists. This is a genuine CICD-SEC-3/4 unpinned-dependency exposure: a takeover/tag-move/malicious release of any of these actions runs attacker code in a job holding the org GitHub App private key, the GPG signing key, and write privileges. It is reachable in the present code (workflows run on PR/push/schedule) and is not mitigated by any existing hardening. Not a duplicate of [Security] critical: OAuth2/JWT access tokens silently escalate to ROLE_SERVICE; issuer/audience validation skipped; session-revocation bypassed #312-[Security] low: Account-lockout failure counter uses non-atomic read-modify-write (race allows extra guesses) #324 (all application-layer auth/crypto). Concrete and specific enough to warrant its own actionable issue while referencing the NFR umbrella. Severity medium is appropriate.

Related

Part of the enterprise security-hardening effort — umbrella tracker #348; security NFR issues #426 (securability), #441 (vulnerability), #362 (confidentiality), #389 (integrity). Prior security wave: #312#324. Not a duplicate of the already-fixed items in that range.


Acceptance = the exploit path is closed AND a regression test (unit/Behat/Schemathesis) proves it stays closed.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions