Skip to content

Commit c9ced12

Browse files
cxbAsDevclaude
andcommitted
fix(chutes): bound OAuth token exchange and userinfo JSON response reads
Add readChutesOAuthJsonResponse helper wrapping readResponseWithLimit with a 4 MB cap to prevent unbounded buffering of Chutes OAuth API responses (userinfo, token exchange, and token refresh). Co-Authored-By: Claude <[email protected]>
1 parent 2fc260a commit c9ced12

1 file changed

Lines changed: 14 additions & 5 deletions

File tree

src/agents/chutes-oauth.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
* for agent model authentication.
44
*/
55
import { createHash, randomBytes } from "node:crypto";
6+
import { readResponseWithLimit } from "@openclaw/media-core/read-response-with-limit";
67
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
78
import { resolveExpiresAtMsFromDurationSeconds } from "../infra/parse-finite-number.js";
89
import type { OAuthCredentials } from "../llm/oauth.js";
910

1011
const CHUTES_OAUTH_ISSUER = "https://api.chutes.ai";
12+
const CHUTES_OAUTH_RESPONSE_MAX_BYTES = 4 * 1024 * 1024;
1113
export const CHUTES_AUTHORIZE_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/authorize`;
1214
export const CHUTES_TOKEN_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/token`;
1315
export const CHUTES_USERINFO_ENDPOINT = `${CHUTES_OAUTH_ISSUER}/idp/userinfo`;
@@ -103,6 +105,13 @@ async function cancelUnreadResponseBody(response: Response): Promise<void> {
103105
}
104106
}
105107

108+
async function readChutesOAuthJsonResponse<T>(response: Response): Promise<T> {
109+
const bytes = await readResponseWithLimit(response, CHUTES_OAUTH_RESPONSE_MAX_BYTES, {
110+
onOverflow: ({ maxBytes }) => new Error(`Chutes OAuth response exceeds ${maxBytes} bytes`),
111+
});
112+
return JSON.parse(new TextDecoder().decode(bytes)) as T;
113+
}
114+
106115
async function fetchChutesUserInfo(params: {
107116
accessToken: string;
108117
fetchFn?: typeof fetch;
@@ -115,7 +124,7 @@ async function fetchChutesUserInfo(params: {
115124
await cancelUnreadResponseBody(response);
116125
return null;
117126
}
118-
const data = (await response.json()) as unknown;
127+
const data = (await readChutesOAuthJsonResponse(response)) as unknown;
119128
if (!data || typeof data !== "object") {
120129
return null;
121130
}
@@ -155,11 +164,11 @@ export async function exchangeChutesCodeForTokens(params: {
155164
throw new Error(`Chutes token exchange failed: ${text}`);
156165
}
157166

158-
const data = (await response.json()) as {
167+
const data = await readChutesOAuthJsonResponse<{
159168
access_token?: string;
160169
refresh_token?: string;
161170
expires_in?: number;
162-
};
171+
}>(response);
163172

164173
const access = data.access_token?.trim();
165174
const refresh = data.refresh_token?.trim();
@@ -226,11 +235,11 @@ export async function refreshChutesTokens(params: {
226235
throw new Error(`Chutes token refresh failed: ${text}`);
227236
}
228237

229-
const data = (await response.json()) as {
238+
const data = await readChutesOAuthJsonResponse<{
230239
access_token?: string;
231240
refresh_token?: string;
232241
expires_in?: number;
233-
};
242+
}>(response);
234243
const access = data.access_token?.trim();
235244
const newRefresh = data.refresh_token?.trim();
236245
const expires = resolveChutesExpiresAt(data.expires_in, now);

0 commit comments

Comments
 (0)