Skip to content

fix(security): validate every generated community-people section, not just rostered ones - #725

Closed
hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/fix-people-sections
Closed

hivecommons-hive[bot] wants to merge 1 commit into
mainfrom
sec/fix-people-sections

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

scripts/validate-community-people.mjs is the only gate CI runs on
data/community-people.json, and the only thing standing between an unattended
upstream refresh and an arbitrary third-party host in an <img src> served to
every visitor of /community and /community/technical-advisory-board.

Its person-level loop was driven by the roster's section keys:

for (const [section, rosterEntries] of Object.entries(roster.sections || {})) {
  const generated = data.people?.[section] || [];

while src/components/CommunityPeople/index.js:134 renders the generated
file's:

const people = peopleData.people[section] || [];

Any section key in data/community-people.json that data/community-roster.json
did not declare therefore skipped the entire loop body — no name check, no
profile-link check, and no profileImageUrl() host gate.

Verified against main @ b54cf81, adding an ambassadors section with
"image": "https://evil.example/beacon.png":

$ node scripts/validate-community-people.mjs
Validated 17 community profiles
$ echo $?
0

Exit 0 — and the summary counted the profile it never checked (17 reported, 16
actually validated). The identical value inside tab is rejected.

The two files agree on their keys today (staff, tab), so this was a latent
hole, not a live exposure. It opens the moment a roster section is renamed or
removed while the generated section survives, or a new section arrives from the
unattended cncf/people refresh — neither of which produced any CI signal.

What this changes

Files touched: scripts/validate-community-people.mjs,
tests/validate-community-people.test.mjs.

  • The loop now runs over the union of both files' section keys, so every
    section the site can render is checked.
  • A generated section the roster does not declare is an error in its own right
    (section is not declared in community-roster.json), reported once for the
    section rather than once per member.
  • The roster-membership cross-check (missing roster member /
    is not on the roster) still only applies within a declared section, so its
    existing messages are unchanged.
  • A declared roster section with a null/absent entry list is treated as empty
    instead of throwing.

The closing Validated N community profiles line is now honest: N already
counted data.people flattened, and every one of those profiles has now
actually been through the checks.

No data changes. data/community-people.json and data/community-roster.json
pass unchanged — this is a gate-only change.

Behaviour change to an existing test

tests/validate-community-people.test.mjs previously asserted, under
treats a roster with no sections as having nothing to cross-check, that a
generated file whose two sections are both absent from the roster exits 0.
That test encoded the hole. It is replaced by
rejects a generated section the roster does not declare, which expects the new
section error.

Verification

  • node --test tests/validate-community-people.test.mjs — 24 pass, 0 fail (5 new)
  • node scripts/validate-community-people.mjs on real repo data — exit 0,
    "Validated 16 community profiles"
  • The ambassadors probe above now exits 1 with both
    section is not declared in community-roster.json and
    Probe image must be an https URL on an allowed host
  • TZ=UTC node tests/tools/coverage-report.mjs --check 97 --check-source 99 —
    exit 0, src files 100.00% line / 97.95% region
  • prettier --check on both changed files — clean

New tests cover: an undeclared section being rejected, the image host gate
firing inside an undeclared section, the roster-membership error not being
repeated per member for an undeclared section, membership still being
cross-checked inside a declared section, and a null roster section list.

Closes #724


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

… 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>
@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.

@mrbobbytables

Copy link
Copy Markdown
Member

Superseded by #753, which consolidates the six open security-fix PRs (commits cherry-picked unmodified, authorship and DCO preserved).

@mrbobbytables
mrbobbytables deleted the sec/fix-people-sections branch September 28, 2026 14:46
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] validate-community-people.mjs loops over roster sections, so a generated-only section skips the image host gate entirely

1 participant