Skip to content

fix(security): consolidated hardening of data validators and URL gates - #753

Merged
mrbobbytables merged 10 commits into
cncf:mainfrom
mrbobbytables:consolidate/security-validators
Sep 27, 2026
Merged

mrbobbytables merged 10 commits into
cncf:mainfrom
mrbobbytables:consolidate/security-validators

Conversation

@mrbobbytables

@mrbobbytables mrbobbytables commented Sep 27, 2026 •

Copy link
Copy Markdown
Member

Summary

Consolidates the seven open security-fix PRs into a single reviewable change. Every commit is an unmodified cherry-pick from its source PR (original authorship and DCO sign-offs preserved). npm run test:unit: 1333 pass, 0 fail.

Also wires the new validate:projects-born gate into ci.yml's validate job, required by main's gate-wiring test (from #742/#750).

Consolidated PRs

PR Fix
#676 treat MDX expression braces as active content in the imported-page gate
#684 add a validator for data/projects-born.json URLs (rendered site-wide as links)
#691 gate SVGs in static/img and static/favicons at the site origin (2 commits)
#717 pin the community-group repository URL gate to https://github.com
#725 validate every generated community-people section, not just rostered ones
#731 parse members.json URLs instead of prefix-matching, rejecting userinfo-spoofed hosts
#756 hold data/metrics.json URLs to a host allow-list (cncf.io, github.com)

The source PRs above can be closed in favor of this one.

Note: this PR touches package.json (new validate:projects-born script from #684) and will need a trivial rebase if #674 merges first.

Closes #675, closes #683, closes #690, closes #716, closes #724, closes #730, closes #752

sec-check and others added 7 commits September 27, 2026 15:13
The imported-page gate reported disallowed elements, event handlers,
script-capable URL schemes and unexpected ESM statements, but never modelled
the one MDX construct that is JavaScript by definition: a braced expression.
Docusaurus compiles docs/architectures/*.md as MDX, and those bodies are
written verbatim from a third-party repository, so upstream text could ship
arbitrary script into the published origin.

Flag every remaining brace after code spans and fences are blanked, allowing
only the inert string-literal attribute form the importer emits itself. Only
the braces of that form are neutralized, so a script URI smuggled into a prop
value is still reported.

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
data/projects-born.json was the only hand-maintained data file feeding an
<a href> with no validator behind it. src/components/ProjectsBorn renders
each entry's url verbatim, and that component is mounted in
src/theme/Footer, so the links ship on every page of the site.

Add scripts/validate-projects-born.mjs, which parses every url and rejects
non-https, userinfo-bearing and unparseable values, and requires name,
origin and description to be non-empty and names to be unique. Parsing
rather than prefix-testing matches checkHttpsUrl() in validate-awards.mjs
and checkUrl() in validate-metrics.mjs: /^https:\/\// accepts
"https://www.cncf.io@evil.example/", whose visible prefix and real host
disagree. No host allow-list, since these are legitimately third-party
project sites.

Registering the script in READ_ONLY_VALIDATORS is what puts it on the pull
request gate; tests/validator-smoke-coverage.test.mjs already enforces that
every scripts/validate-*.mjs appears there.

Signed-off-by: hivecommons-hive[bot] <hivecommons-hive@hive.kubestellar.io>
…e origin

scripts/validate-architecture-assets.mjs states that it gates every
directory published verbatim at the site origin, but assetDirs listed only
static/img/architectures, static/img/cncf-projects and static/img/awards.
static/img itself and static/favicons were never walked, so the five SVGs
served from them - including the footer logo and both favicons - reached
the origin with no active-content check and no extension allow-list.

SVG is a document format: a browser that navigates directly to one parses
it as XML and executes any script it carries, in the site's own origin. A
pull request refreshing a logo or favicon therefore had no automated gate,
while the identical file one directory deeper was rejected.

Add both roots. static/img is walked shallowly because its image
subdirectories are already listed with their own quality settings, so
walk() takes a recurse flag; the symlink check runs before the directory
branch so a symlinked directory is still reported in a shallow walk.
static/img also serves the legacy favicon.ico, so the two chrome roots use
an extension set of ALLOWED_ASSET_EXTENSIONS plus .ico - kept separate
because ALLOWED_ASSET_EXTENSIONS mirrors what the importer mirrors, and
the importer never writes an .ico. ICO is a raster container no browser
parses as markup.

static/fonts and the static/ root stay outside the gate: no SVG, and
extensions legitimately outside the image allow-list.

The five previously ungated SVGs already pass unchanged; this is a
gate-only change. Coverage goes from 69 to 82 published assets.

Signed-off-by: hivecommons-hive[bot] <hivecommons-hive@hive.kubestellar.io>
The new shallow walk of static/img enumerated static/img/architectures,
static/img/cncf-projects and static/img/awards as ordinary entries. Each
of those is already an asset root with its own entry in assetDirs, which
decides for itself whether a symlink is an error and whether a
non-directory root is skipped.

Re-entering them from the parent walk duplicated the symlink error and,
where a root was a regular file rather than a directory, rejected it as
an extensionless asset - breaking the pre-existing
'a regular file at an asset root is skipped, not walked' test.

Skip any entry whose path is itself a declared asset root.

Signed-off-by: hivecommons-hive[bot] <hivecommons-hive@hive.kubestellar.io>
…ithub.com

validate-community-groups.mjs checked group.repository with a bare
new URL() parse, which succeeds for javascript:, data:, cleartext http,
and a userinfo-spoofed authority such as https://github.com@evil.example.
It was the only URL gate in data/ that decided nothing about the
destination beyond parseability, while validate-case-studies.mjs,
validate-radar-reports.mjs, validate-awards.mjs, validate-architectures.mjs
and lib/project-card-links.mjs all require https, reject userinfo, and pin
the host.

Require https, reject userinfo, and pin the host to github.com, which is
the only host check-community-group-links.mjs ever writes. The
missing-field branch still reports an absent repository on its own.

Signed-off-by: hivecommons-hive[bot] <hivecommons-hive@hive.kubestellar.io>
… just rostered ones

scripts/validate-community-people.mjs drove its person-level loop from
data/community-roster.json's section keys, while
src/components/CommunityPeople renders data/community-people.json's. A
section present only in the generated file therefore skipped every check
in the loop body, including the profileImageUrl() host gate, and reached
<img src> with an arbitrary host.

The loop now runs over the union of both files' section keys, and a
generated section the roster does not declare is an error in its own
right.

Signed-off-by: hivecommons-hive[bot] <hivecommons-hive@hive.kubestellar.io>
data/members.json renders as <a href> on /community/members via
MemberProfile.js, but it has no validator script: there is no
validate:members, nothing for it in ci.yml's validator list, and it is
absent from READ_ONLY_VALIDATORS in validators-smoke.test.mjs. Its only
gate was the /^https:\/\// prefix regex in members-data.test.mjs.

That is the test validate-awards.mjs and validate-architectures.mjs
already document as insufficient: "https://www.cncf.io@evil.example/"
matches the prefix while resolving to evil.example, and the bare string
"https://" matches while failing to parse at all. The awards
cross-check does not compensate, because it compares only the
slug/year/award key and never the URL values.

Gate architectures[].sourceUrl, the three award URLs and every
sourceAttribution entry on a parsed URL that must use https and must
carry no userinfo, mirroring isHttpsUrl() in validate-architectures.mjs,
and cover the spoofed forms.

Signed-off-by: hivecommons-hive[bot] <hivecommons-hive@hive.kubestellar.io>
mrbobbytables and others added 3 commits September 27, 2026 15:19
The consolidated security fixes add the validate:projects-born gate, and
main now asserts that every validate:*/check:* script is run by a
workflow or recorded as exempt. Wire the new gate into ci.yml alongside
the other data validators, which also unblocks the narrow coverage-report
runs that execute workflow-scripts.test.mjs as a fixture.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Bob Killen <bkillen@linuxfoundation.org>
scripts/validate-metrics.mjs was the only validator guarding a rendered
href that applied no host allow-list: checkUrl() asserted an absolute
https URL with no userinfo and stopped, so any host passed.

Every URL in data/metrics.json is rendered under hard-coded anchor text
-- 'cncf/architecture' in src/components/ReferenceArchitectures, 'Source'
in src/components/MetricsDashboard -- so a plain https URL on a host that
is not CNCF's publishes as a CNCF-labelled link to an unrelated origin.
That is the same sink shape validate-case-studies.mjs and
validate-radar-reports.mjs already pin with ALLOWED_HOST_SUFFIXES.

Pin the host to cncf.io or github.com after the existing userinfo check,
which keeps the more specific userinfo diagnostic for a spoofed-userinfo
URL. Both hosts are already the only ones present in data/metrics.json,
so this is a no-op for the current data.

Closes cncf#752

Signed-off-by: sec-check <sec-check@hive.kubestellar.io>
@mrbobbytables
mrbobbytables added this pull request to the merge queue Sep 27, 2026
Merged via the queue into cncf:main with commit 10e70b5 Sep 27, 2026
5 checks passed
mrbobbytables added a commit to mrbobbytables/endusers that referenced this pull request Sep 29, 2026
CONTRIBUTING.md's data contribution model claimed twice that
data/projects-born.json has no validation script. PR cncf#753 added
scripts/validate-projects-born.mjs and wired npm run
validate:projects-born into CI, but no doc mentioned it.

Updated the table row and the rules bullet to reference the
validator.

Fixes cncf#776

Signed-off-by: mrbobbytables <mrbobbytables@users.noreply.github.com>
Co-authored-by: mrbobbytables <mrbobbytables@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment