Skip to content

Commit 0dcb0c4

Browse files
committed
fix(msteams): bound Bot Framework User Token JSON response read
1 parent c432c8c commit 0dcb0c4

3 files changed

Lines changed: 114 additions & 1 deletion

File tree

extensions/msteams/src/monitor-handler.sso.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,44 @@ describe("handleSigninTokenExchangeInvoke", () => {
116116
expect(stored?.expiresAt).toBe("2030-01-01T00:00:00Z");
117117
});
118118

119+
it("returns a service error when the User Token response exceeds the byte cap", async () => {
120+
const hugeBody = JSON.stringify({
121+
channelId: "msteams",
122+
connectionName: "GraphConnection",
123+
token: "delegated-graph-token",
124+
expiration: "2030-01-01T00:00:00Z",
125+
padding: "x".repeat(128 * 1024),
126+
});
127+
const fetchImpl: MSTeamsSsoFetch = async () =>
128+
new Response(
129+
new ReadableStream({
130+
start(controller) {
131+
controller.enqueue(new TextEncoder().encode(hugeBody));
132+
controller.close();
133+
},
134+
}),
135+
{ status: 200, headers: { "content-type": "application/json" } },
136+
);
137+
const { sso, tokenStore } = createSsoDeps({ fetchImpl });
138+
139+
const result = await handleSigninTokenExchangeInvoke({
140+
value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
141+
user: { userId: "aad-user-guid", channelId: "msteams" },
142+
deps: sso,
143+
});
144+
145+
expect(result.ok).toBe(false);
146+
if (!result.ok) {
147+
expect(result.code).toBe("unexpected_response");
148+
expect(result.message).toMatch(/invalid JSON/i);
149+
}
150+
const stored = await tokenStore.get({
151+
connectionName: "GraphConnection",
152+
userId: "aad-user-guid",
153+
});
154+
expect(stored).toBeNull();
155+
});
156+
119157
it("returns a service error when the User Token service rejects the exchange", async () => {
120158
const { fetchImpl } = createFakeFetch([
121159
() => ({ ok: false, status: 502, body: "bad gateway" }),

extensions/msteams/src/sso.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* that ack; these helpers encapsulate token exchange and persistence.
2525
*/
2626

27+
import { readProviderJsonResponse } from "openclaw/plugin-sdk/provider-http";
2728
import type { MSTeamsAccessTokenProvider } from "./attachments/types.js";
2829
import { readMSTeamsHttpErrorDetail } from "./http-error.js";
2930
import type { MSTeamsSsoTokenStore } from "./sso-token-store.js";
@@ -35,6 +36,9 @@ const BOT_FRAMEWORK_TOKEN_SCOPE = "https://api.botframework.com/.default";
3536
/** Bot Framework User Token service base URL. */
3637
const BOT_FRAMEWORK_USER_TOKEN_BASE_URL = "https://token.botframework.com";
3738

39+
/** Max bytes to read from the User Token service JSON response. */
40+
const USER_TOKEN_RESPONSE_MAX_BYTES = 64 * 1024;
41+
3842
/**
3943
* Response shape returned by the Bot Framework User Token service for
4044
* `GetUserToken` and `ExchangeToken`.
@@ -130,7 +134,9 @@ async function callUserTokenService(
130134
}
131135
let parsed: unknown;
132136
try {
133-
parsed = await response.json();
137+
parsed = await readProviderJsonResponse<unknown>(response, "Bot Framework User Token service", {
138+
maxBytes: USER_TOKEN_RESPONSE_MAX_BYTES,
139+
});
134140
} catch {
135141
return { error: "invalid JSON from User Token service", status: response.status };
136142
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Real behavior proof: MSTeams SSO token exchange bounds the User Token
2+
// service JSON response so an oversized body cannot OOM the runtime.
3+
//
4+
// The proof runs a local HTTP server that returns a User Token response
5+
// larger than the 64 KiB cap. With the fix the handler rejects with an
6+
// "invalid JSON" service error; without the bound the runtime would buffer
7+
// the entire oversized body before parsing.
8+
9+
import { createServer } from "node:http";
10+
import type { AddressInfo } from "node:net";
11+
import { handleSigninTokenExchangeInvoke } from "../../extensions/msteams/src/sso.js";
12+
13+
const hugeBody = JSON.stringify({
14+
channelId: "msteams",
15+
connectionName: "GraphConnection",
16+
token: "delegated-graph-token",
17+
expiration: "2030-01-01T00:00:00Z",
18+
padding: "x".repeat(128 * 1024),
19+
});
20+
21+
const server = createServer((req, res) => {
22+
res.writeHead(200, { "Content-Type": "application/json" });
23+
res.end(hugeBody);
24+
});
25+
26+
await new Promise<void>((resolve, reject) => {
27+
server.listen(0, "127.0.0.1", () => {
28+
resolve();
29+
});
30+
server.on("error", reject);
31+
});
32+
const { port } = server.address() as AddressInfo;
33+
34+
console.log("=== Proof: MSTeams SSO JSON response bound ===\n");
35+
36+
try {
37+
const result = await handleSigninTokenExchangeInvoke({
38+
value: { id: "flow-1", connectionName: "GraphConnection", token: "exchangeable-token" },
39+
user: { userId: "aad-user-guid", channelId: "msteams" },
40+
deps: {
41+
userTokenBaseUrl: `http://127.0.0.1:${port}`,
42+
connectionName: "GraphConnection",
43+
tokenProvider: {
44+
getAccessToken: async () => "bf-service-token",
45+
},
46+
tokenStore: {
47+
async get() {
48+
return null;
49+
},
50+
async save() {},
51+
async remove() {
52+
return true;
53+
},
54+
},
55+
},
56+
});
57+
58+
if (!result.ok && result.code === "unexpected_response" && /invalid JSON/i.test(result.message)) {
59+
console.log("PASS: oversized User Token JSON response was rejected without OOM.");
60+
} else {
61+
console.log("FAIL: unexpected result:", result);
62+
process.exitCode = 1;
63+
}
64+
} catch (err) {
65+
console.error("FAIL: handler threw:", err);
66+
process.exitCode = 1;
67+
} finally {
68+
server.close();
69+
}

0 commit comments

Comments
 (0)