Skip to content

[sec-check] deploy-gh-pages.yml grants id-token: write and pages: write workflow-wide — the npm build job holds an OIDC-minting and Pages-deploy credential it never uses #374

Description

@hivecommons-hive

Security Finding

Severity: high
Type: permission-issue (CI least-privilege / supply-chain blast radius)
File: .github/workflows/deploy-gh-pages.yml (lines 9-12, job build, job deploy)

.github/workflows/deploy-gh-pages.yml declares its token scopes once, at the workflow level:

permissions:
  contents: read
  pages: write
  id-token: write

Workflow-level permissions: apply to every job. There are two jobs, and only one of them needs those scopes:

job what it runs what it actually needs
build npm ci, npm run test:unit, the four validators, npm run build:production, actions/configure-pages, actions/upload-pages-artifact contents: read + pages: read
deploy actions/deploy-pages only — no checkout, no npm pages: write + id-token: write

So the build job — the one that installs and executes several hundred third-party npm packages and renders imported third-party architecture content — runs holding both id-token: write and pages: write, neither of which any step in it reads.

actions/configure-pages is pinned here at 983d7736 (v5.0.0) and is invoked without the enablement input, which defaults to false; in that mode it only reads the Pages API, so pages: read is sufficient for the build job. (If enablement: true were ever set, the action documents that GITHUB_TOKEN cannot satisfy it at all, so this does not become a reason to keep pages: write.)

Impact

id-token: write is not an inert flag. When a job is granted it, the runner injects ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN into that job's environment, and any process in the job — including an npm postinstall script from any transitive dependency, or anything reachable from the imported-content build path — can read those two variables and mint a signed GitHub OIDC token asserting repo:cncf/endusers. That token is a bearer credential against every cloud role, registry, or workload-identity trust policy anywhere that trusts this repository's OIDC subject. Nothing in the build job needs it, so today that credential exists only as attacker reward.

pages: write in the same job is the second half: a compromised build step can call the Pages API directly and publish arbitrary content to the live site, without going through upload-pages-artifact → deploy-pages at all, and without tripping the github-pages environment protection that gates the deploy job. Scoping pages: write to the deploy job is what makes that environment gate meaningful.

This is the standard hardening actions/deploy-pages is designed for — the split-job pattern in its own README exists precisely so the build job never holds the deployment credential. It is also a pattern this repository already uses elsewhere: .github/workflows/pdf.yml declares permissions: at the job level, not the workflow level.

Related but distinct, already tracked, and deliberately not part of this issue:

Recommendation

Move the grants to the jobs that use them. Replace the workflow-level block at lines 9-12 with an empty default, and add a permissions: block to each job.

Replace:

permissions:
  contents: read
  pages: write
  id-token: write

with:

permissions: {}

Then in the build job, add permissions: immediately after runs-on: ubuntu-latest:

  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: read
    steps:

and in the deploy job, add permissions: immediately after needs: build:

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    permissions:
      pages: write
      id-token: write
    steps:

No step, action pin, env: value, or concurrency: setting changes. actions/checkout keeps contents: read; actions/upload-pages-artifact needs no GITHUB_TOKEN scope (it uses the artifact runtime token); actions/deploy-pages keeps exactly the two scopes it requires.

Verification

After the change:

  • build completes: checkout, npm ci, npm run test:unit, all four validators, npm run build:production, configure-pages, upload-pages-artifact all succeed under contents: read + pages: read.
  • deploy completes and the site publishes, confirming pages: write + id-token: write on that job is sufficient.
  • The build job log shows no OIDC token request env vars in scope.

This change is compatible with the token-permission assertions in open PR #301 (tests/ci-supply-chain.test.mjs): that suite requires every workflow to declare permissions: at workflow or job level and forbids write-all, and the layout above satisfies both.

Why this is an issue and not a pull request

The fix lives entirely in .github/workflows/. This agent's token is minted at the contributor tier, which does not carry the GitHub Actions workflows permission, so a push touching that directory is rejected server-side. This needs a human, or an agent whose token carries the workflows permission, to land — it is not a judgement call that the change is unsuitable for a PR. The replacement text above is complete and can be applied as-is.


Filed by sec-check agent (ACMM L4/L5 — hold-gated mode)

🐝 Hive Agent: security | Instance: hosted-available-lke648397-260827-5n31 | SHA: 00b44df

— hive: agent=sec-check backend=copilot model=claude-opus-5

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/securityApproved by a Hive merger/owner for auto-merge on green CIblockedWaiting on something outside this repository; not contributor work until a human clears the labelhive/hosted-available-lke648397-260827-5n31Approved by a Hive merger/owner for auto-merge on green CIsecurityApproved by a Hive merger/owner for auto-merge on green CI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions