Skip to content

fix: validate architecture catalog sourceUrl scheme and asset containment (scripts/validate-architectures.mjs) - #334

Open
kubestellar-hive[bot] wants to merge 1 commit into
mainfrom
sec/validate-architectures-url-containment
Open

kubestellar-hive[bot] wants to merge 1 commit into
mainfrom
sec/validate-architectures-url-containment

Conversation

@kubestellar-hive

Copy link
Copy Markdown
Contributor

Security Fix

scripts/validate-architectures.mjs is the only gate between
data/architectures/catalog.json — regenerated nightly from the third-party
cncf/architecture repository — and the published site. It checked neither of
the two fields that reach a render sink.

sourceUrl was not validated at all. scripts/generate-members.mjs:145-146
copies it into members.json sourceAttribution, which
src/components/MemberDirectory/index.js:196 renders as an href with no
scheme guard. A javascript: value passed npm run validate:architectures and
reached the published site.

assets[] containment was never asserted. The existence probe built its
path with join(root, 'static', asset.replace(/^\//, '')), which normalises
.. away, so the probe escaped static/ entirely.

Changes (all in scripts/validate-architectures.mjs)

  • isHttpsUrl()sourceUrl must be present and parse as an https: URL via
    new URL(), not a prefix or substring test.
  • resolveContainedAsset() — every assets[] entry must start with
    /img/architectures/ and, after resolution, still be contained in that
    directory. Resolve first, then assert the prefix, so .. cannot normalise the
    guard away. existsSync runs only on a contained path. This is the same
    resolve-then-contain ordering [sec-check] validate-awards.mjs logo containment check is defeated by '..' — path escapes static/img/awards/ and existsSync probes the build runner #327 records for validate-awards.mjs.
  • id must match /^[a-z0-9][a-z0-9-]*$/, since it is used both as a route
    segment (/architectures/<id>) and as a filesystem path component in
    import-architectures.mjs.

Verification

Clean catalog still validates:

$ node scripts/validate-architectures.mjs
Validated 7 architecture records   # exit 0

A catalog carrying a javascript: sourceUrl, an escaping asset, an asset with
an interior .., and a ../evil id is now rejected:

4 error(s) in architecture catalog:
  [error] adobe: sourceUrl must be an https URL; it is rendered as an href in the member directory
  [error] adobe: asset /../../../../etc/hostname must be a site-absolute path contained in /img/architectures/
  [error] allianz: asset /img/architectures/../../../etc/hostname must be a site-absolute path contained in /img/architectures/
  [error] ../evil: id must be a lowercase slug matching /^[a-z0-9][a-z0-9-]*$/; ...
# exit 1

npx prettier --check scripts/validate-architectures.mjs fails identically on
main and on this branch — the file is part of the pre-existing 62-file
formatting baseline tracked in #305, so it is deliberately not reformatted here.

Scope

Touches exactly one file, scripts/validate-architectures.mjs, and only the
sourceUrl / assets[] / id validation cluster. Disjoint from the open work
on import-architectures.mjs (#195, #212, #216, #249, #266),
validate-metrics.mjs (#304, #263), validate-awards.mjs (#329),
validate-architecture-assets.mjs (#231) and generate-members.mjs (#208).

Closes #332


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

…sset containment

scripts/validate-architectures.mjs is the only gate between
data/architectures/catalog.json -- regenerated nightly from the third-party
cncf/architecture repository -- and the published site, but it checked neither
of the two fields that reach a render sink.

- sourceUrl was not validated at all. generate-members.mjs copies it into
  members.json sourceAttribution, which MemberDirectory renders as an href
  with no scheme guard, so a javascript: value passed validation and reached
  the published site.
- assets[] containment was never asserted, and the existence probe built its
  path with join(root, 'static', asset.replace(/^\//, '')), which normalises
  '..' away -- so the probe escaped static/ entirely.

sourceUrl must now parse as an https: URL via new URL(). Assets must start with
/img/architectures/ and, after resolution, still be contained in that directory
-- resolve first, then assert the prefix, so '..' cannot normalise the guard
away. id must be a lowercase slug, since it is used both as a route segment and
as a filesystem path component.

Verified: the clean catalog still validates (7 records, exit 0); a catalog
carrying a javascript: sourceUrl, an escaping asset, an asset with interior
'..', and a '../evil' id is rejected with four errors and exit 1.

Signed-off-by: kubestellar-hive[bot] <kubestellar-hive@hive.kubestellar.io>
@kubestellar-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.

@kubestellar-hive kubestellar-hive Bot added security Approved by a Hive merger/owner for auto-merge on green CI agent/security Approved by a Hive merger/owner for auto-merge on green CI hive/hosted-available-lke648397-260827-5n31 Approved by a Hive merger/owner for auto-merge on green CI labels Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent/security Approved by a Hive merger/owner for auto-merge on green CI hive/hosted-available-lke648397-260827-5n31 Approved by a Hive merger/owner for auto-merge on green CI hold security Approved by a Hive merger/owner for auto-merge on green CI

Projects

None yet

0 participants