Skip to content

fix(oauth): guard GHE Copilot device-flow requests to the caller's gheUrl - #15046

Merged
diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.51from
HouMinXi:fix/ghe-copilot-oauth-ssrf
Sep 29, 2026
Merged

diegosouzapw merged 1 commit into
diegosouzapw:release/v3.8.51from
HouMinXi:fix/ghe-copilot-oauth-ssrf

Conversation

@HouMinXi

@HouMinXi HouMinXi commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The ghe-copilot device flow takes gheUrl from the request (the query string
for device-code, extraData for poll) and builds four outbound requests from
it. The only check was that the URL is https, and the requests used plain
fetch, which follows redirects. An https host the caller controls can answer
with a redirect to a plain-http internal address or to the cloud metadata
service, and the device-code route hands the JSON it gets back to the caller.
/api/oauth/ is on the public route list and the handler accepts any valid
API key, so an ordinary client key is enough to do this.

Send those requests through safeOutboundFetch with the provider outbound
guard, the same policy other operator-supplied provider URLs use: private
hosts stay reachable for on-premises GitHub Enterprise Server, metadata and
link-local addresses are refused, and a redirect ends the request instead of
being followed. GitHub Enterprise Server serves these paths on the configured
host itself, so the normal flow does not depend on following a redirect.

Metadata addresses stay blocked when the operator has allowed private
provider URLs, since a GHE host is never one. What the host answers is no
longer relayed as is: the device-code response is cut down to the fields the
flow uses, upstream error bodies are not put into error messages, and poll
answers only carry the token fields and a known device-flow error code. The
poll handler also read the response body twice on the non-JSON path, which
threw instead of reporting the bad answer. The two lookups after login now
leave their fields empty when the host redirects them, like they already did
for a non-2xx answer.

Related Issues

  • None. This fixes a defect found by review, not a filed issue.

Validation

  • Change type: other
  • Focused tests: tests/unit/oauth-ghe-url-ssrf.test.ts
  • npm run lint
  • Reconciled with the current active release base
  • Production-code changes include a new or updated automated test in this PR

Tests Added Or Updated

  • tests/unit/oauth-ghe-url-ssrf.test.ts

Coverage Notes

  • The change is covered by the test files listed above. No coverage drop is expected; the new tests exercise the paths this PR adds.

Reviewer Notes

  • gheUrl is now checked before the four outbound device-flow requests. A URL that is not the caller's own GHE host is rejected.

…eUrl

The ghe-copilot device flow takes gheUrl from the request (the query string
for device-code, extraData for poll) and builds four outbound requests from
it. The only check was that the URL is https, and the requests used plain
fetch, which follows redirects. An https host the caller controls can answer
with a redirect to a plain-http internal address or to the cloud metadata
service, and the device-code route hands the JSON it gets back to the caller.
/api/oauth/ is on the public route list and the handler accepts any valid
API key, so an ordinary client key is enough to do this.

Send those requests through safeOutboundFetch with the provider outbound
guard, the same policy other operator-supplied provider URLs use: private
hosts stay reachable for on-premises GitHub Enterprise Server, metadata and
link-local addresses are refused, and a redirect ends the request instead of
being followed. GitHub Enterprise Server serves these paths on the configured
host itself, so the normal flow does not depend on following a redirect.

Metadata addresses stay blocked when the operator has allowed private
provider URLs, since a GHE host is never one. What the host answers is no
longer relayed as is: the device-code response is cut down to the fields the
flow uses, upstream error bodies are not put into error messages, and poll
answers only carry the token fields and a known device-flow error code. The
poll handler also read the response body twice on the non-JSON path, which
threw instead of reporting the bad answer. The two lookups after login now
leave their fields empty when the host redirects them, like they already did
for a non-2xx answer.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
@diegosouzapw
diegosouzapw merged commit e201782 into diegosouzapw:release/v3.8.51 Sep 29, 2026
9 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants