Skip to content

fix: gate case-study link targets on a cncf.io host allow-list - #501

Merged
mrbobbytables merged 1 commit into
mainfrom
sec/case-study-url-host-gate
Sep 23, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
sec/case-study-url-host-gate

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

scripts/collect-case-studies.mjs copies the cncf.io WordPress REST API's
post.link into data/case-studies.json verbatim, and
src/components/CaseStudies/index.js:190 renders every one of them as
<a href={study.url} target="_blank"> under a CNCF organization name on
/resources/case-studies. The only gate between the two was an
/^https:\/\// prefix test in scripts/validate-case-studies.mjs, which
is not a host check. Verified at 8c089a0:

true  https://www.cncf.io@evil.example/case-studies/acme/   -> hostname evil.example
true  https://www.cncf.io.evil.example/x                    -> hostname www.cncf.io.evil.example
true  https://evil.example/x                                -> hostname evil.example

This change parses the URL instead and holds the host to a cncf.io
allow-list, rejecting non-https, userinfo-bearing, and off-host targets — the
same standard scripts/lib/profile-image.mjs already applies to the profile
images published beside them. A rejected entry now names the offending value so
a maintainer can act on it, while the existing missing or duplicate url and
sourceUrl must be an https URL wording is preserved for the cases those
already describe.

All 211 URLs currently in data/case-studies.json are on www.cncf.io, so the
allow-list is a no-op for today's data and only catches drift.

Verification

  • npm run validate:case-studies — Validated 211 case studies, clean on real data.
  • node --test tests/validate-case-studies.test.mjs — 14/14 pass (7 added: off-host,
    prefix-only host, userinfo spoof and non-https, for both sourceUrl and entry
    url, plus an accepted apex-cncf.io case).
  • npm run test:unit — 554 tests / 498 pass / 54 fail, against a 8c089a0
    baseline of 547 / 491 / 54 fail in the same sandbox: +7 tests, +7 passing,
    identical pre-existing failure count, no regression.
  • npx prettier --check scripts/validate-case-studies.mjs tests/validate-case-studies.test.mjs — clean.

Disjointness

Touches exactly two files: scripts/validate-case-studies.mjs and
tests/validate-case-studies.test.mjs. No shared helper is added or modified —
the gate is local to the validator precisely so it cannot collide.

scripts/validate-radar-reports.mjs:45 carries the identical weakness and is
deliberately excluded: open PR #496 claims that validator's entry-url check
("test: cover validate-radar-reports sourceUrl, title and entry-url checks").
Issue #500 records that remainder and says it should be filed and fixed
separately once #496 lands. No other open PR names either file here: #195/#212
cover imported architecture SVG/MDX, #266 project logos, #291 check:links,
#225 tests/tools/coverage-report.mjs, #435 ArchitectureFilters, #476
pr-queue-hygiene, #479 ProjectsBorn, #482 GroupLinkStatus/PeopleFreshness,
#486 CommunityPeople, #490 ReferenceArchitectures.

Closes #500 — that issue's scope is the case-studies validator only, and the
radar remainder it names is explicitly deferred to its own issue, so merging
this leaves nothing for #500 to track.


Filed by sec-check agent (ACMM L4/L5 — hold-gated mode). Hold-gated: human review required.

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

scripts/collect-case-studies.mjs copies the cncf.io WordPress API's post.link
into data/case-studies.json verbatim, and src/components/CaseStudies renders
every one as an <a href> under a CNCF organization name. The only gate was an
/^https:\/\// prefix test, which accepts any host and accepts a userinfo
component that resolves to a hostile host while reading as CNCF in a diff.

Parse the URL and hold the host to a cncf.io allow-list, matching the standard
scripts/lib/profile-image.mjs already applies to profile images.

Closes #500

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Important

Held for human review by the hive's ACMM level gate.

This PR was opened by the "sec-check" agent while Hive policy required a human checkpoint for that agent. Non-outreach agents are held at ACMM L3–L5; the outreach agent is always held because it publishes project-facing communication.

Hive will automatically remove the hold label once current policy no longer requires a level hold for "sec-check". If this is an outreach PR, a human must review it and remove the label.

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

Labels

Projects

None yet

1 participant