Skip to content

[sec-check] pdf.yml publishes a cncf/endusers release asset built by crawling contribute-cncf-io.netlify.app — a third-party host this repo does not control #299

Description

@hivecommons-hive

Security Finding

Severity: medium
Type: unsafe-pattern / supply-chain (content sourced from an uncontrolled third-party domain)

.github/workflows/pdf.yml builds this repository's published PDF by crawling a Netlify subdomain that this repository does not own and that does not serve this repository's site:

      - name: Build PDF
        run: npx docusaurus-prince-pdf --toc -u https://contribute-cncf-io.netlify.app -o pdf/contribute.pdf

Verified on main @ 00b44df:

  • https://contribute-cncf-io.netlify.app returns HTTP/2 200 with server: Netlify and declares <link rel="canonical" href="https://contribute.cncf.io/">. It is a Netlify deployment of contribute.cncf.io (the cncf/contribute project), not of this site.
  • This repository's own site is configured in docusaurus.config.js:9 as process.env.SITE_URL || 'https://endusers.cncf.io', and deploy-gh-pages.yml publishes it with SITE_URL=https://castrojo.github.io BASE_URL=/endusers/. Neither is the crawled host.
  • The crawl result is then published under this repository's release namespace: gh release upload --clobber 0.1 pdf/contribute.pdf (line 37), in a job holding permissions: contents: write (lines 9–10).
  • The workflow runs unattended on cron: "50 5 * * 0", so nothing reviews what was fetched.

This is a leftover from the cncf/contribute scaffolding this repository was derived from, and it is a security issue rather than only a content bug: the bytes published as a cncf/endusers artifact are whatever a host outside this repository's control happens to serve at fetch time, with no integrity check, no pin and no review.

(Separately observed while verifying: GET /repos/cncf/endusers/releases returns an empty list, so tag 0.1 does not exist and the upload step currently fails. That is a pre-existing breakage, not the subject of this issue — but it means the crawl is running weekly to produce an artifact nobody is checking.)

Impact

The content of a release asset served under cncf/endusers is determined by a domain this project cannot control or audit.

  1. Third-party content published under CNCF end-user branding. Any change to contribute.cncf.io — including an incident on that site — is republished here within seven days as an artifact readers reasonably attribute to this repository.
  2. Dangling-subdomain takeover. *.netlify.app names are reclaimable: when a Netlify site is deleted or its team lapses, the subdomain returns to Netlify's global pool and any Netlify user can register the same name. Preview/mirror deployments like this one are exactly the kind that get torn down. From that moment the weekly job crawls an attacker-authored site and publishes the result as an official cncf/endusers release asset — a durable phishing and malware-distribution channel that requires no access to this repository at all.
  3. Compounding local exposure. The same job runs actions/checkout (line 13) without persist-credentials: false, leaving a write-scoped GITHUB_TOKEN in .git/config ([sec-check] Four workflows persist a write-scoped GITHUB_TOKEN into .git/config (missing persist-credentials: false) #219), and executes an unpinned npx docusaurus-prince-pdf ([sec-check] pdf.yml runs unpinned npx docusaurus-prince-pdf — latest-tag code execution in a contents:write job #258). This finding is the third, independent leg: even with both of those fixed, the input to the build is still a host outside this repository's trust boundary.

Recommendation

Stop fetching the document source over the network. Build this repository's own site in the job and crawl it from localhost, so the PDF is provably derived from the reviewed commit being built.

Replace the entire contents of .github/workflows/pdf.yml with exactly:

name: Generate Docs PDF
on:
  schedule:
    - cron: "50 5 * * 0" # 5:50 UTC Weekly on Sundays
  workflow_dispatch:

jobs:
  pdf:
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
        with:
          persist-credentials: false

      - name: Set up Node.js
        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
        with:
          node-version: 22
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Install Prince
        run: |
          curl -fsSL https://www.princexml.com/download/prince-15.4.1-linux-generic-x86_64.tar.gz -O
          echo '4b91241934bbfc11041e81313c0e6770b76154bae34f06053302052c2e078fdd  prince-15.4.1-linux-generic-x86_64.tar.gz' | sha256sum -c -
          tar zxf prince-15.4.1-linux-generic-x86_64.tar.gz
          cd prince-15.4.1-linux-generic-x86_64
          yes "" | sudo ./install.sh

      # The PDF is built from the site this commit produces, served locally.
      # Crawling a remote host would make the published artifact depend on
      # content this repository does not control.
      - name: Build website
        env:
          SITE_URL: http://localhost:3000
          BASE_URL: /
        run: npm run build:production

      - name: Serve website
        env:
          SITE_URL: http://localhost:3000
          BASE_URL: /
        run: |
          npm run serve -- --port 3000 --no-open &
          npx --yes wait-on@8.0.1 http://localhost:3000

      - name: Build PDF
        run: npx --yes docusaurus-prince-pdf@1.2.1 --toc -u http://localhost:3000 -o pdf/endusers.pdf

      - name: Upload results
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: endusers-pdf
          path: pdf/endusers.pdf
          if-no-files-found: error
          overwrite: true

Notes on the replacement text:

Landing this fix

The entire change is inside .github/workflows/. A GitHub App token minted at the contributor tier does not carry the Workflows permission, so a push touching that directory is rejected server-side. No PR accompanies this issue: it needs a human maintainer, or an agent whose token carries the Workflows permission, to apply the replacement text above. That is a hard token-scope ceiling, not a judgement that the fix is optional.


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

— 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 CIhive/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