Skip to content

Commit 6d362db

Browse files
authored
fix(minimax): guard oauth token fetches (#88088)
1 parent 1fd5a90 commit 6d362db

1 file changed

Lines changed: 62 additions & 40 deletions

File tree

extensions/minimax/oauth.ts

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { randomBytes, randomUUID } from "node:crypto";
22
import { resolveExpiresAtMsFromDurationOrEpoch } from "openclaw/plugin-sdk/number-runtime";
33
import { generatePkceVerifierChallenge, toFormUrlEncoded } from "openclaw/plugin-sdk/provider-auth";
44
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
5+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
56

67
export type MiniMaxRegion = "cn" | "global";
78

@@ -28,6 +29,7 @@ function getOAuthEndpoints(region: MiniMaxRegion) {
2829
tokenEndpoint: `${config.baseUrl}/oauth/token`,
2930
clientId: config.clientId,
3031
baseUrl: config.baseUrl,
32+
hostname: new URL(config.baseUrl).hostname,
3133
};
3234
}
3335

@@ -78,39 +80,47 @@ async function requestOAuthCode(params: {
7880
region: MiniMaxRegion;
7981
}): Promise<MiniMaxOAuthAuthorization> {
8082
const endpoints = getOAuthEndpoints(params.region);
81-
const response = await fetch(endpoints.codeEndpoint, {
82-
method: "POST",
83-
headers: {
84-
"Content-Type": "application/x-www-form-urlencoded",
85-
Accept: "application/json",
86-
"x-request-id": randomUUID(),
83+
const { response, release } = await fetchWithSsrFGuard({
84+
url: endpoints.codeEndpoint,
85+
init: {
86+
method: "POST",
87+
headers: {
88+
"Content-Type": "application/x-www-form-urlencoded",
89+
Accept: "application/json",
90+
"x-request-id": randomUUID(),
91+
},
92+
body: toFormUrlEncoded({
93+
response_type: "code",
94+
client_id: endpoints.clientId,
95+
scope: MINIMAX_OAUTH_SCOPE,
96+
code_challenge: params.challenge,
97+
code_challenge_method: "S256",
98+
state: params.state,
99+
}),
87100
},
88-
body: toFormUrlEncoded({
89-
response_type: "code",
90-
client_id: endpoints.clientId,
91-
scope: MINIMAX_OAUTH_SCOPE,
92-
code_challenge: params.challenge,
93-
code_challenge_method: "S256",
94-
state: params.state,
95-
}),
101+
policy: { allowedHostnames: [endpoints.hostname] },
102+
auditContext: "minimax.oauth.code",
96103
});
104+
try {
105+
if (!response.ok) {
106+
const text = await response.text();
107+
throw new Error(`MiniMax OAuth authorization failed: ${text || response.statusText}`);
108+
}
97109

98-
if (!response.ok) {
99-
const text = await response.text();
100-
throw new Error(`MiniMax OAuth authorization failed: ${text || response.statusText}`);
101-
}
102-
103-
const payload = (await response.json()) as MiniMaxOAuthAuthorization & { error?: string };
104-
if (!payload.user_code || !payload.verification_uri) {
105-
throw new Error(
106-
payload.error ??
107-
"MiniMax OAuth authorization returned an incomplete payload (missing user_code or verification_uri).",
108-
);
109-
}
110-
if (payload.state !== params.state) {
111-
throw new Error("MiniMax OAuth state mismatch: possible CSRF attack or session corruption.");
110+
const payload = (await response.json()) as MiniMaxOAuthAuthorization & { error?: string };
111+
if (!payload.user_code || !payload.verification_uri) {
112+
throw new Error(
113+
payload.error ??
114+
"MiniMax OAuth authorization returned an incomplete payload (missing user_code or verification_uri).",
115+
);
116+
}
117+
if (payload.state !== params.state) {
118+
throw new Error("MiniMax OAuth state mismatch: possible CSRF attack or session corruption.");
119+
}
120+
return payload;
121+
} finally {
122+
await release();
112123
}
113-
return payload;
114124
}
115125

116126
async function pollOAuthToken(params: {
@@ -119,20 +129,32 @@ async function pollOAuthToken(params: {
119129
region: MiniMaxRegion;
120130
}): Promise<TokenResult> {
121131
const endpoints = getOAuthEndpoints(params.region);
122-
const response = await fetch(endpoints.tokenEndpoint, {
123-
method: "POST",
124-
headers: {
125-
"Content-Type": "application/x-www-form-urlencoded",
126-
Accept: "application/json",
132+
const { response, release } = await fetchWithSsrFGuard({
133+
url: endpoints.tokenEndpoint,
134+
init: {
135+
method: "POST",
136+
headers: {
137+
"Content-Type": "application/x-www-form-urlencoded",
138+
Accept: "application/json",
139+
},
140+
body: toFormUrlEncoded({
141+
grant_type: MINIMAX_OAUTH_GRANT_TYPE,
142+
client_id: endpoints.clientId,
143+
user_code: params.userCode,
144+
code_verifier: params.verifier,
145+
}),
127146
},
128-
body: toFormUrlEncoded({
129-
grant_type: MINIMAX_OAUTH_GRANT_TYPE,
130-
client_id: endpoints.clientId,
131-
user_code: params.userCode,
132-
code_verifier: params.verifier,
133-
}),
147+
policy: { allowedHostnames: [endpoints.hostname] },
148+
auditContext: "minimax.oauth.token",
134149
});
150+
try {
151+
return await parseMiniMaxOAuthTokenResponse(response);
152+
} finally {
153+
await release();
154+
}
155+
}
135156

157+
async function parseMiniMaxOAuthTokenResponse(response: Response): Promise<TokenResult> {
136158
const text = await response.text();
137159
let payload:
138160
| {

0 commit comments

Comments
 (0)