You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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
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:
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.
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.
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.
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 PDFon:
schedule:
- cron: "50 5 * * 0"# 5:50 UTC Weekly on Sundaysworkflow_dispatch:
jobs:
pdf:
permissions:
contents: writeruns-on: ubuntu-lateststeps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5with:
persist-credentials: false
- name: Set up Node.jsuses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0with:
node-version: 22cache: npm
- name: Install dependenciesrun: npm ci
- name: Install Princerun: | 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 websiteenv:
SITE_URL: http://localhost:3000BASE_URL: /run: npm run build:production
- name: Serve websiteenv:
SITE_URL: http://localhost:3000BASE_URL: /run: | npm run serve -- --port 3000 --no-open & npx --yes wait-on@8.0.1 http://localhost:3000
- name: Build PDFrun: npx --yes docusaurus-prince-pdf@1.2.1 --toc -u http://localhost:3000 -o pdf/endusers.pdf
- name: Upload resultsuses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4with:
name: endusers-pdfpath: pdf/endusers.pdfif-no-files-found: erroroverwrite: true
Notes on the replacement text:
The -u target is now http://localhost:3000, served from this commit's own npm run build:production output. No third-party domain is contacted for document content.
pdf/contribute.pdf becomes pdf/endusers.pdf and the artifact name becomes endusers-pdf, because the document is now this site, not contribute.cncf.io.
The Upload Release Artifact step is dropped. It targets release tag 0.1, which does not exist on cncf/endusers (GET /repos/cncf/endusers/releases returns []), so it fails on every run today. With it gone the job no longer needs a token at all; if a release upload is wanted later, add it back against a tag that exists, in its own reviewed change.
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)
Security Finding
Severity: medium
Type: unsafe-pattern / supply-chain (content sourced from an uncontrolled third-party domain)
.github/workflows/pdf.ymlbuilds 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:Verified on
main@00b44df:https://contribute-cncf-io.netlify.appreturnsHTTP/2 200withserver: Netlifyand declares<link rel="canonical" href="https://contribute.cncf.io/">. It is a Netlify deployment of contribute.cncf.io (thecncf/contributeproject), not of this site.docusaurus.config.js:9asprocess.env.SITE_URL || 'https://endusers.cncf.io', anddeploy-gh-pages.ymlpublishes it withSITE_URL=https://castrojo.github.io BASE_URL=/endusers/. Neither is the crawled host.gh release upload --clobber 0.1 pdf/contribute.pdf(line 37), in a job holdingpermissions: contents: write(lines 9–10).cron: "50 5 * * 0", so nothing reviews what was fetched.This is a leftover from the
cncf/contributescaffolding 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/releasesreturns an empty list, so tag0.1does 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/endusersis determined by a domain this project cannot control or audit.*.netlify.appnames 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.actions/checkout(line 13) withoutpersist-credentials: false, leaving a write-scopedGITHUB_TOKENin.git/config([sec-check] Four workflows persist a write-scoped GITHUB_TOKEN into .git/config (missing persist-credentials: false) #219), and executes an unpinnednpx docusaurus-prince-pdf([sec-check] pdf.yml runs unpinnednpx 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.ymlwith exactly:Notes on the replacement text:
-utarget is nowhttp://localhost:3000, served from this commit's ownnpm run build:productionoutput. No third-party domain is contacted for document content.pdf/contribute.pdfbecomespdf/endusers.pdfand the artifact name becomesendusers-pdf, because the document is now this site, not contribute.cncf.io.Upload Release Artifactstep is dropped. It targets release tag0.1, which does not exist oncncf/endusers(GET /repos/cncf/endusers/releasesreturns[]), so it fails on every run today. With it gone the job no longer needs a token at all; if a release upload is wanted later, add it back against a tag that exists, in its own reviewed change.persist-credentials: falseis set on the checkout, closing this workflow's leg of [sec-check] Four workflows persist a write-scoped GITHUB_TOKEN into .git/config (missing persist-credentials: false) #219.npx --yes docusaurus-prince-pdf@1.2.1applies the pin requested in [sec-check] pdf.yml runs unpinnednpx docusaurus-prince-pdf— latest-tag code execution in a contents:write job #258. [sec-check] pdf.yml runs unpinnednpx docusaurus-prince-pdf— latest-tag code execution in a contents:write job #258 proposes an edit to the same line 23; applying this replacement satisfies [sec-check] pdf.yml runs unpinnednpx docusaurus-prince-pdf— latest-tag code execution in a contents:write job #258 forpdf.ymlas well, so apply one or the other, not both.Landing this fix
The entire change is inside
.github/workflows/. A GitHub App token minted at thecontributortier 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