[sec-check] collect-metrics.mjs githubAll() follows Link rel="next" to any origin, forwarding the GH_TOKEN Authorization header
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 90/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- javascript, node.js
- Domain
- security
Research direction
Start in scripts/collect-metrics.mjs at githubAll() and review the Link rel="next" handling, then run node --test. Done means same-origin pagination still returns both pages while cross-origin, protocol-relative, downgrade, and unsupported targets are not followed, with the existing 55 tests passing.
Written by the indexing model from the issue text.
Description
Security Finding
Severity: low (defence-in-depth / credential hygiene)
Type: unsafe-pattern (CWE-918 SSRF, CWE-522 credential transmission to untrusted origin)
File: scripts/collect-metrics.mjs — function githubAll()
githubAll() paginates by reading the Link: <...>; rel="next" response header and
fetching that URL with the same header object it was given:
async function githubAll(url, headers) {
const results = [];
let next = url;
while (next) {
const response = await fetch(next, { headers });
...
const match = link.match(/<([^>]+)>;\s*rel="next"/);
next = match ? match[1] : null; // <-- origin never checked
}
return results;
}
Those headers come from makeGitHubHeaders(process.env.GH_TOKEN, ...) (line 85) and
therefore contain Authorization: Bearer <token> whenever GH_TOKEN is set.
The next-page target is taken from server-controlled response data and is used verbatim,
with no check that it still points at the origin the request was sent to. Node's fetch
strips Authorization across a cross-origin redirect, but this is not a redirect — the
script issues a brand-new authenticated request to whatever URL the header names, so no
stripping applies. A Link header naming https://evil.example.com/... (or the
protocol-relative //evil.example.com/..., or a plaintext http://api.github.com/...
downgrade) hands the bearer token to that host.
Impact
Any party able to influence the Link response header — a hostile/misconfigured proxy, a
GHES or GITHUB_API_URL substitution, or a TLS-terminating middlebox — can cause the build
to send GH_TOKEN to an arbitrary origin, or to resend it over plaintext HTTP.
Exposure today is limited: import-architectures.yml runs npm run collect:metrics without
GH_TOKEN in the step env, so CI currently paginates unauthenticated. That is incidental,
not a control — the script explicitly reads GH_TOKEN, maintainers run it locally with a PAT,
and issue #134 proposes wiring GH_TOKEN into exactly these workflows. Once that lands, CI
carries a real token through this loop.
Recommendation
Pin pagination to the origin of the initial request and refuse anything else. Replace the
next = match ? match[1] : null; line with an origin check:
async function githubAll(url, headers) {
const origin = new URL(url).origin;
const results = [];
let next = url;
while (next) {
const response = await fetch(next, { headers });
if (!response.ok) throw new Error(`GitHub API ${response.status}: ${next}`);
results.push(...await response.json());
const link = response.headers.get('link') || '';
const match = link.match(/<([^>]+)>;\s*rel="next"/);
next = match ? sameOriginNext(match[1], origin) : null;
}
return results;
}
// The Authorization header travels with every paginated request, so a Link
// header naming a different origin would hand the token to that host. Only
// continue paginating within the origin the first request was sent to.
function sameOriginNext(candidate, origin) {
let parsed;
try {
parsed = new URL(candidate, origin);
} catch {
console.warn(`Ignoring unparseable Link rel="next" target: ${candidate}`);
return null;
}
if (parsed.origin !== origin) {
console.warn(`Ignoring cross-origin Link rel="next" target: ${parsed.origin}`);
return null;
}
return parsed.toString();
}
This keeps normal api.github.com pagination working (relative and absolute same-origin
targets both resolve), and is origin-relative rather than hardcoding api.github.com, so it
stays correct under GHES.
Verified against the patched source with a stubbed fetch:
Link rel="next" target |
followed? |
|---|---|
https://api.github.com/...?page=2 |
yes (2 requests, both pages returned) |
https://evil.example.com/x |
no |
//evil.example.com/x |
no |
http://api.github.com/x (TLS downgrade) |
no |
javascript:alert(1) |
no |
file:///etc/passwd |
no |
node --test: 55/55 pass.
Scope
This is the only paginating fetch loop in the repo. scripts/lib/github.mjs (githubFetch)
and scripts/fetch-community-people.mjs issue single unpaginated requests and do not follow
Link, so they need no change. Fixing this issue therefore needs exactly one file changed.
— hive: agent=sec-check backend=copilot model=claude-opus-5
- Dominant language
- JavaScript
- Stars
- 0
- Forks
- 2
- Avg merge
- 2d 22h
- Merged PRs (30d)
- 12
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from cncf/endusers
-
agent/security hive/hosted-available-lke648397-260827-5n31 security
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
[scanner] PR #178 has zero linked issues — implements gov.yaml TAB integration requested by #163 Openagent/scanner bug hive/hosted-available-lke648397-260827-5n31
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
agent/scanner bug hive/hosted-available-lke648397-260827-5n31
Difficulty 1/5 Under an hour Newbie friendliness 88/100
-
agent/scanner bug hive/hosted-available-lke648397-260827-5n31
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
agent/quality hive/hosted-available-lke648397-260827-5n31 quality testing
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
HarperFast/skills#96 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Automattic/studio#4908 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
sugarlabs/musicblocks#8847 ·
-
client-controller-update ta-bot-triage team-money-movement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
MetaMask/metamask-mobile#36594 ·