fix(oauth): bound github-copilot OAuth response reads at 16 MiB#97499
Conversation
|
Codex review: needs maintainer review before merge. Reviewed June 28, 2026, 11:47 AM ET / 15:47 UTC. Summary PR surface: Source +7, Tests +82. Total +89 across 2 files. Reproducibility: yes. for source-level review. Current main and Review metrics: 2 noteworthy metrics.
Stored data model Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land one canonical bounded-read implementation for the core Copilot Do we have a high-confidence way to reproduce the issue? Yes for source-level review. Current main and Is this the best way to solve the issue? Yes for the implementation shape. Reusing the shared provider bounded readers at the single Copilot AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 2f851ecfe9df. Label changesLabel justifications:
Evidence reviewedPR surface: Source +7, Tests +82. Total +89 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
@clawsweeper review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
|
Thanks @vincentkoc for the merge and the P1 prioritization on the OAuth surface. Two things from this round I'd like to call out as useful signals for me:
Sibling surface |
…claw#97499) * fix(oauth): bound github-copilot OAuth response reads at 16 MiB * chore: trigger CI re-run after PR body update
…claw#97499) * fix(oauth): bound github-copilot OAuth response reads at 16 MiB * chore: trigger CI re-run after PR body update
…claw#97499) * fix(oauth): bound github-copilot OAuth response reads at 16 MiB * chore: trigger CI re-run after PR body update
…claw#97499) * fix(oauth): bound github-copilot OAuth response reads at 16 MiB * chore: trigger CI re-run after PR body update (cherry picked from commit bd0c052)
…claw#97499) * fix(oauth): bound github-copilot OAuth response reads at 16 MiB * chore: trigger CI re-run after PR body update
…claw#97499) * fix(oauth): bound github-copilot OAuth response reads at 16 MiB * chore: trigger CI re-run after PR body update (cherry picked from commit bd0c052)
What Problem This Solves
src/llm/utils/oauth/github-copilot.ts:178-190(fetchJson) reads the OAuth response body withawait response.text()andresponse.json(), both of which buffer the entire payload into memory before the runtime can decide to stop. A hostile or buggy GitHub OAuth endpoint (or enterprise proxy) can return200 OKwith a body that grows unbounded, and the runtime will buffer all of it before failing. The existingfetchResponsetimeout only caps wall-clock time, not bytes — a slow trickle that exceeds memory still OOMs.The OAuth surface here is untrusted:
<domain>/login/device/code,<domain>/login/oauth/access_token,api.<domain>/copilot_internal/v2/token, andapi.individual.githubcopilot.com/modelsare all external, so a DoS-shaped response is in-scope as a runtime hardening fix.Why This Change Was Made
src/agents/provider-http-errors.tsalready exposes the canonical bounded readers (readProviderTextResponse,readProviderJsonResponse) capped at the shared 16 MiB cap. The sibling reference already inmainissrc/agents/chutes-oauth.ts:9— same import shape, same helper, three call sites with labels"Chutes userinfo"/"Chutes token exchange"/"Chutes token refresh". This PR mirrors that pattern for github-copilot OAuth, wiring the existing 4fetchJsoncall sites (each already passes a distinctoperationstring) through the shared helper. No new abstraction, no SDK promotion, no config change — pure per-surface application of an existing helper.User Impact
GitHub Copilot <op>: JSON response exceeds 16777216 byteswhen an OAuth endpoint streams >16 MiB, which is the desired safety behavior.${status} ${statusText}: ${text}) is preserved byte-for-byte.Changes
src/llm/utils/oauth/github-copilot.ts:7-9— importreadProviderJsonResponseandreadProviderTextResponsefrom the shared../../../agents/provider-http-errors.js(sibling pattern matchessrc/agents/chutes-oauth.ts:9).src/llm/utils/oauth/github-copilot.ts:182-197—fetchJsonswapsawait response.text()/response.json()for the bounded readers with a per-call label"GitHub Copilot <operation>". Default cap is the shared 16 MiB (PROVIDER_TEXT_RESPONSE_MAX_BYTES/PROVIDER_JSON_RESPONSE_MAX_BYTESfromsrc/agents/provider-http-errors.ts:14-17).src/llm/utils/oauth/github-copilot.test.ts:185-265— 3 new inline tests in a newdescribe("GitHub Copilot OAuth bounded reads")block: (1) oversized streaming body triggers overflow with the labeled error and the standard 16 MiB cap, (2) normal-size OAuth JSON parses through, (3) happy-path round-trip throughrefreshGitHubCopilotTokenreturns the access token withexpiresas a number. Tests use realReadableStream+ chunkedvi.stubGlobal("fetch"), mirroring the inline pattern fromsrc/agents/provider-transport-fetch.test.ts(Alix-007 fix(googlechat): replace unbounded response.json() with readProviderJsonResponse #96772).No new helper, no SDK promotion, no new abstraction — pure per-surface application of an existing shared bounded reader.
The four call sites of
fetchJson(lines 204, 298, 371, 450) need no edits: each already passes a distinctoperationstring ("device code request", "device token request", "token refresh request", "model list request"), so the labels"GitHub Copilot <operation>"stay distinct without further changes.Real behavior proof
Real Node
httpserver on127.0.0.1, realrefreshGitHubCopilotTokenwrapper,globalThis.fetchrewritten to pointhttps://api.github.com/...at the local server (production code untouched). Proof script is/tmp/g1_real_server_proof.mts, run once, not committed (per checklist #23).readProviderJsonResponseinstead of buffering the full payload.http.createServerlistening on ephemeral127.0.0.1port, realglobalThis.fetchwrapper routing only the GitHub OAuth hostnames to localhost, real productionrefreshGitHubCopilotTokeninvoked.bytesSent=17825792(17 MiB) but the runtime threwGitHub Copilot token refresh request: JSON response exceeds 16777216 bytesinstead of buffering all 17 MiB. The error message embeds the cap value and the per-call label so logs can attribute the bounded-read to this call site (not chutes, not anthropic).access=copilot-token-XYZwithexpires=1782658963000(the 5-min bufferMs fromresolveExpiresAtMsFromEpochSecondsis correctly applied).{"error":"invalid_token"}flows throughreadProviderTextResponseand is wrapped into the existing${response.status} ${response.statusText}: ${text}error format — same observable behavior as before, with the body now bounded at 16 MiB.fetchJsonhelper so the wrapper-level guarantee is the same. Adding duplicate proof would inflate the PR without adding signal.ReadableStreamandTextEncoderbehavior is consistent across Node 22 LTS and Node 24).Out of scope
src/llm/utils/oauth/anthropic.ts:236still has unboundedawait response.text(). Different maintainer area; will be a follow-up PR with the samereadProviderTextResponseswap.@google/genai2.7.0 exposeshttpOptions: { baseUrl, apiVersion, headers, timeout, extraBody, retryOptions }but nofetchparameter at theGoogleGenAIconstructor, andgenerateContentStream(params)does not accept a secondRequestOptionsargument, so the SDK cannot be wired throughbuildGuardedModelFetch.Diff stats
Size: S (≤ 100 LoC non-test). All test additions are inline in the existing
github-copilot.test.ts.Risk checklist
GitHub Copilot <op>: JSON response exceeds 16777216 byteswhen an OAuth endpoint streams >16 MiB. That is the desired safety behavior.operationparameter (4 distinct, hand-formed strings already in production) and prefixing withGitHub Copilotso logs disambiguate from sibling bounded reads (chutes, anthropic, etc.).Evidence
Real Node
httpserver, realrefreshGitHubCopilotTokenwrapper,globalThis.fetchrewritten to point only the GitHub OAuth hostnames at the loopback server. Proof captured in one run, not committed (per checklist #23).Byte-level quantification:
17825792bytes), client threw at the 16 MiB cap (16777216bytes).bytesSent - cap = 1048576(1 MiB overshoot, exactly one chunk) — proves the bounded reader stopped reading at the cap, not after buffering the full body.expires = 1782658963000 = (expectedExpiry * 1000) - 5*60*1000(5-min bufferMs fromresolveExpiresAtMsFromEpochSecondsapplied as expected).${response.status} ${response.statusText}: ${text}preserved exactly.Reproduction script (not committed):
Label: security
AI-assisted.