Skip to content

fix: validate community roster GitHub handles before the API fetch - #316

Open
kubestellar-hive[bot] wants to merge 1 commit into
mainfrom
sec/roster-handle-validation
Open

kubestellar-hive[bot] wants to merge 1 commit into
mainfrom
sec/roster-handle-validation

Conversation

@kubestellar-hive

Copy link
Copy Markdown
Contributor

Security Fix

scripts/fetch-community-people.mjs interpolated the github field from
data/community-roster.json directly into an api.github.com request path,
with no validation and no percent-encoding:

const response = await fetch(`https://api.github.com/users/${github}`, { headers });

The literal prefix pins the origin, so this was never a cross-origin token
leak — api.github.com cannot be replaced. What it allowed was endpoint
confusion: a handle such as ../../orgs/evil resolves to
https://api.github.com/orgs/evil, and that response body is what gets written
into data/community-people.json (name, company, bio, location,
blog, avatar_url) and rendered by src/components/CommunityPeople.
.github/workflows/refresh-community-people.yml runs this on a schedule and
opens the refresh PR automatically, so a malformed handle propagates without
the roster diff being re-read.

What this changes

scripts/fetch-community-people.mjs only:

  • Adds GITHUB_HANDLE, matching GitHub's username grammar (alphanumerics plus
    single internal hyphens, 1-39 characters), and exits non-zero with the
    offending entry named when a roster handle fails it. A malformed handle is a
    roster defect, so failing the scheduled refresh surfaces it rather than
    quietly publishing a substituted profile.
  • Percent-encodes the handle at the interpolation point as defence in depth.
  • Replaces the hand-rolled Authorization header with makeGitHubHeaders() from
    scripts/lib/github.mjs, so this call site inherits centralised auth
    handling the way scripts/collect-metrics.mjs does. The default
    User-Agent in that helper is the same cncf-endusers-site-build string
    that was inlined here, so request headers are unchanged.

Falsy handles keep their existing meaning: the two roster entries with
github: null are still skipped, not rejected.

Verification

  • All 16 real handles in data/community-roster.json satisfy the new
    predicate, so the committed roster is unaffected and generated output is
    unchanged.
  • node --test tests/ — 67/67 pass (55 before, 12 new).
  • New tests/community-roster-handles.test.mjs runs entirely offline via the
    existing runScriptWithFixtures helper: every fixture entry uses
    github: null so no network request is ever issued, and only the rejected
    handle decides the exit status.
  • npx prettier --check is clean on the new test file. The pre-existing
    prettier violations in scripts/fetch-community-people.mjs are left alone —
    they are present on main at 00b44df and reformatting the file would
    widen this diff into unrelated lines.

Scope / overlap

Claims scripts/fetch-community-people.mjs (the GITHUB_HANDLE predicate,
the header construction, and the fetch call) plus the new file
tests/community-roster-handles.test.mjs. Checked against the open PR set:

Closes #313


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/fetch-community-people.mjs interpolated the roster 'github' field
straight into the api.github.com request path with no validation and no
percent-encoding, so a handle containing a path separator or dot segment
re-pointed the request at a different endpoint and published that response
as somebody's profile.

Reject any handle that is not a GitHub username (alphanumerics plus single
internal hyphens, 1-39 chars) before fetching, percent-encode at the
interpolation point, and reuse makeGitHubHeaders() from scripts/lib/github.mjs
instead of hand-rolling the Authorization header.

Closes #313

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 19, 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

Development

Successfully merging this pull request may close these issues.

[sec-check] fetch-community-people.mjs interpolates unvalidated roster handles into the api.github.com request path

0 participants