mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-09 03:49:52 +02:00
## Summary
Hardens the local `github-artifacts` MCP helper
(`tools/mcp/github-artifacts/server.js`) so the `GITHUB_TOKEN` bearer
token is only ever sent to allowlisted GitHub API hosts. Previously the
same token was attached when downloading image and ZIP URLs extracted
from issue/PR Markdown, which could forward the token to any host
referenced in untrusted issue content.
This is repository MCP dev-tooling, not the shipped PowerToys runtime.
## What changed
- **New `auth.js`** centralizes token handling:
- `AUTH_ALLOWED_HOSTS = { api.github.com }`
- `shouldSendGitHubToken(url)` — true only for HTTPS + allowlisted host
- `headersForUrl(url, token, extra)` — attaches `Authorization` only
when the host is allowlisted
- **`server.js`** routes all three request sites (`fetchJson`,
`downloadBytes`, `downloadZipBytes`) through `headersForUrl`. Image/ZIP
URLs from issue content are now fetched **without** `Authorization`.
Removed the ZIP dual-attempt auth retry that previously sent the token
to untrusted hosts.
- **New `test-token-allowlist.js`** regression test (wired into `npm
test` / `npm run test:unit`).
## Verification
- Unit test: 13/13 pass — token attached for `api.github.com`, withheld
from arbitrary hosts, loopback, look-alike hostnames
(`api.github.com.attacker.com`), non-HTTPS, and
`*.githubusercontent.com`.
- Cross-origin redirects: confirmed Node's `fetch` (undici) strips
`Authorization` when a redirect leaves the origin.
- Functionality preserved (real public issues, real token): issue #25595
images (5/5 downloaded) and issue #39476 `PowerToysReport_*.zip`
(downloaded + extracted) both succeed without the token — GitHub serves
this content via public/signed URLs.
## Note
For private-repo attachments that would require an `Authorization`
header on a non-API host, the token is intentionally not sent (the
recommended tradeoff). PowerToys is public, so the helper's normal use
is unaffected.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
36 lines
1.3 KiB
JavaScript
36 lines
1.3 KiB
JavaScript
// Centralized control over when the GitHub bearer token may be attached to an
|
|
// outgoing request.
|
|
//
|
|
// Image and ZIP URLs are extracted from untrusted issue/PR Markdown and can
|
|
// point to any host. The GitHub bearer token (GITHUB_TOKEN) must therefore only
|
|
// ever be sent to explicitly allowlisted GitHub API hosts, never to URLs that
|
|
// originate from issue content. Sending it elsewhere would leak the token to an
|
|
// attacker-controlled server.
|
|
|
|
export const AUTH_ALLOWED_HOSTS = new Set([
|
|
"api.github.com"
|
|
]);
|
|
|
|
// Returns true only when it is safe to attach the GitHub bearer token to the
|
|
// given URL: the request must be HTTPS and target an allowlisted GitHub API host.
|
|
export function shouldSendGitHubToken(rawUrl) {
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(rawUrl);
|
|
} catch {
|
|
return false;
|
|
}
|
|
return parsed.protocol === "https:" && AUTH_ALLOWED_HOSTS.has(parsed.hostname.toLowerCase());
|
|
}
|
|
|
|
// Builds request headers, attaching Authorization only when the target URL is an
|
|
// allowlisted GitHub API host. Arbitrary image/ZIP URLs from issue content never
|
|
// receive the token.
|
|
export function headersForUrl(rawUrl, token, extra = {}) {
|
|
return {
|
|
...extra,
|
|
...(token && shouldSendGitHubToken(rawUrl) ? { "Authorization": `Bearer ${token}` } : {}),
|
|
"User-Agent": "issue-images-mcp"
|
|
};
|
|
}
|