Skip to content

test(security): hold end user talk links to an https host allow-list - #1375

Closed
hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/end-user-talks-link-gate
Closed

hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/end-user-talks-link-gate

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

data/kubecon-na-2026-end-user-talks.json is the only data feed this site
renders as <a href> that no gate validates.
src/components/EndUserTalkSchedule/index.js renders schedule.sourceUrl
under the fixed anchor text "live official schedule", schedule.apiUrl under
"official Sessionize API", 83 talk.url values under their talk titles, and
#${day.id} for the day jump links — 85 hrefs whose destination the anchor
text never shows.

Every sibling feed is gated: scripts/validate-metrics.mjs,
scripts/validate-case-studies.mjs and scripts/validate-radar-reports.mjs
each parse their URLs and hold the host to an allow-list, rejecting a userinfo
component (https://events.linuxfoundation.org@evil.example/) that resolves to
a hostile host while reading as the official one in a skimmed JSON diff.

tests/end-user-talk-schedule.test.mjs cannot supply that gate: it asserts the
component faithfully reproduces whatever the corpus says, so it stays green on
a re-pointed URL — its anchor filter matches the /program/schedule/?id=
substring, which both the userinfo and the …linuxfoundation.org.evil.example
spellings keep.

What this adds

One file, tests/end-user-talks-link-safety.test.mjs:

  • sourceUrl and every talk.url must be https:, carry no userinfo, and
    resolve to hostname events.linuxfoundation.org; apiUrl to
    sessionize.com — the two hosts scripts/collect-end-user-talks.mjs builds
    its output from.
  • every talk.url must stay on the schedule page's own pathname, so a link
    cannot be re-aimed elsewhere on the same host.
  • generatedAt must parse (it is rendered through
    new Date(...).toLocaleDateString(), which publishes the literal string
    "Invalid Date" rather than failing the build).
  • day.id must be a plain lowercase slug and unique, since it is written into
    both an element id and an href="#...".
  • publishableUrl() itself is pinned against the ten rejection shapes, so the
    guard cannot pass vacuously if the corpus is ever empty.

Why a test rather than a validate:end-user-talks script

tests/workflow-scripts.test.mjs requires every validate:*/check:* entry
in package.json to be run by a step in .github/workflows/, or recorded in
GATES_NOT_RUN_BY_CI as a deliberate omission. Recording it there would leave
the hole open while looking closed. The unit suite is already run on both paths
this needs to block — ci.yml:40 (test:unit:coverage:check) and
deploy-gh-pages.yml:41 (test:unit) — so a test file closes it with no
workflow change and no exemption. Issue #1374 records the exact two workflow
steps for a maintainer who prefers the validate:* shape for consistency; that
variant needs a human or an ISSUES_PRS_MERGE agent, because an
ISSUES_AND_PRS token is minted at the contributor tier and GitHub rejects
any push touching .github/workflows/**. This PR does not depend on it and
is complete on its own.

Verification

  • node --test tests/end-user-talks-link-safety.test.mjs — 4 pass.
  • Negative check: setting sourceUrl to
    https://events.linuxfoundation.org@evil.example/program/schedule/ and one
    talk.url to https://events.linuxfoundation.org.evil.example/… fails two
    of the four assertions with the offending value named;
    tests/end-user-talk-schedule.test.mjs passes on that same mutated corpus,
    which is the gap this closes. Corpus restored afterwards — the diff is the
    one new test file and nothing else.
  • tests/workflow-scripts.test.mjs + tests/refresh-script-wiring.test.mjs —
    26 pass. validator-smoke-coverage, e2e-coverage-describe-gate,
    e2e-data-dependent-skips, site-config (the suites that enumerate
    tests/) — 49 pass.
  • npx prettier --check clean. No source file is added, so the
    --check-source-file-regions / --require-source-files obligations are
    unchanged: SOURCE_ROOTS in tests/tools/coverage-report.mjs is
    scripts, src, tests/tools, and a tests/*.test.mjs file is neither
    source nor harness.

Disjointness from the open hold-gated PRs

This PR adds exactly one new file and modifies none.

Closes #1374


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 copilot=1.0.88

data/kubecon-na-2026-end-user-talks.json is the only data feed rendered as
<a href> that no gate validates. src/components/EndUserTalkSchedule renders
sourceUrl, apiUrl and 83 talk.url values as links whose destination the anchor
text never shows, while validate-metrics, validate-case-studies and
validate-radar-reports each hold their own feed's URLs to an https host
allow-list that rejects a userinfo component.

tests/end-user-talk-schedule.test.mjs cannot supply that gate: it asserts the
component reproduces whatever the corpus says, and its anchor filter matches a
'/program/schedule/?id=' substring a hostile host keeps.

The gate is a test rather than a validate:* script because workflow-scripts
requires every validate:*/check:* entry to be run by a workflow step or
recorded as exempt, and the unit suite is already run by ci.yml and
deploy-gh-pages.yml.

Closes #1374

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 keep the hold label until a human removes it. Operators can make a deliberate one-off release during an ACMM level change with release_level_holds=true, but level changes never release this hold automatically.

@mrbobbytables

Copy link
Copy Markdown
Member

Superseded by #1406, which folds these assertions into tests/end-user-talks-data.test.mjs (landed from #1383) so the dataset has a single contract file, and reuses the shared checkUrl() from #1381 instead of a local publishableUrl() copy. The generatedAt and day.id checks here were already covered by #1383.

mrbobbytables added a commit to mrbobbytables/endusers that referenced this pull request Oct 15, 2026
Fold the URL gate proposed in cncf#1375 into tests/end-user-talks-data.test.mjs
so the snapshot has one integrity contract rather than two files. Reuse
checkUrl() from scripts/lib/validate-utils.mjs (landed in cncf#1381) instead of
adding another copy of the scheme/userinfo/host check; sourceUrl and every
talk.url must resolve to events.linuxfoundation.org and stay on the
schedule page's pathname, apiUrl to sessionize.com.

Closes cncf#1374

Signed-off-by: mrbobbytables <mrbobbytables@users.noreply.github.com>
Co-authored-by: mrbobbytables <mrbobbytables@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mrbobbytables
mrbobbytables deleted the sec/end-user-talks-link-gate branch October 16, 2026 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] data/kubecon-na-2026-end-user-talks.json renders 85 hrefs with no host allow-list, unlike every sibling data feed

1 participant