Skip to content

Commit b77f3dc

Browse files
committed
fix(msteams): bound federated certificate file reads
createFederatedApp read the certificate path with an unbounded fs.readFileSync, so a misconfigured or oversized certificate file could trigger an unbounded read at credential-load time. Adopt tryReadSecretFileSync (the same helper the credential-reader sweep in #109260 applied to anthropic-vertex / google / gh-read / mantis) with a 1 MiB maxBytes bound so oversized certificate files are rejected at the size check rather than read whole. Missing or empty files surface as the existing "Failed to read certificate file" error. Update the sdk tests to mock tryReadSecretFileSync and add a regression case asserting an oversized file (helper returns undefined) fails the load with the bound applied.
1 parent e0478e2 commit b77f3dc

2 files changed

Lines changed: 50 additions & 7 deletions

File tree

extensions/msteams/src/sdk.test.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
// Msteams tests cover sdk plugin behavior.
2-
import * as fs from "node:fs";
32
import { afterEach, describe, expect, it, vi } from "vitest";
43
import { createMSTeamsTokenProvider, loadMSTeamsSdkWithAuth } from "./sdk.js";
54
import type { MSTeamsCredentials, MSTeamsFederatedCredentials } from "./token.js";
@@ -14,6 +13,15 @@ vi.mock("node:fs", async (importOriginal) => {
1413
};
1514
});
1615

16+
const { tryReadSecretFileSyncMock } = vi.hoisted(() => ({
17+
tryReadSecretFileSyncMock: vi.fn<
18+
(filePath: string, label: string, options: unknown) => string | undefined
19+
>(() => "-----BEGIN RSA PRIVATE KEY-----\nfake-key\n-----END RSA PRIVATE KEY-----"),
20+
}));
21+
vi.mock("openclaw/plugin-sdk/secret-file-runtime", () => ({
22+
tryReadSecretFileSync: tryReadSecretFileSyncMock,
23+
}));
24+
1725
const { mockGetToken } = vi.hoisted(() => {
1826
const mockGetTokenLocal = vi.fn().mockResolvedValue({ token: "mock-managed-token" });
1927
return { mockGetToken: mockGetTokenLocal };
@@ -76,13 +84,15 @@ describe("createMSTeamsApp", () => {
7684

7785
const app = await createMSTeamsApp(creds);
7886
expect(app).toBeDefined();
79-
expect(fs.readFileSync).toHaveBeenCalledWith("/path/to/cert.pem", "utf-8");
87+
expect(tryReadSecretFileSyncMock).toHaveBeenCalledWith(
88+
"/path/to/cert.pem",
89+
"MS Teams federated certificate",
90+
expect.objectContaining({ maxBytes: 1024 * 1024 }),
91+
);
8092
});
8193

8294
it("throws when certificate file is missing", async () => {
83-
vi.mocked(fs.readFileSync).mockImplementation(() => {
84-
throw new Error("ENOENT: no such file");
85-
});
95+
tryReadSecretFileSyncMock.mockReturnValueOnce(undefined);
8696

8797
const creds: MSTeamsFederatedCredentials = {
8898
type: "federated",
@@ -94,6 +104,26 @@ describe("createMSTeamsApp", () => {
94104
await expect(createMSTeamsApp(creds)).rejects.toThrow("Failed to read certificate file");
95105
});
96106

107+
it("throws when the certificate file exceeds the size bound", async () => {
108+
// tryReadSecretFileSync returns undefined for an oversized file (its own
109+
// maxBytes guard rejects it); the load path must surface that as a failure.
110+
tryReadSecretFileSyncMock.mockReturnValueOnce(undefined);
111+
112+
const creds: MSTeamsFederatedCredentials = {
113+
type: "federated",
114+
appId: "test-app-id",
115+
tenantId: "test-tenant",
116+
certificatePath: "/path/to/huge.pem",
117+
};
118+
119+
await expect(createMSTeamsApp(creds)).rejects.toThrow("Failed to read certificate file");
120+
expect(tryReadSecretFileSyncMock).toHaveBeenCalledWith(
121+
"/path/to/huge.pem",
122+
"MS Teams federated certificate",
123+
expect.objectContaining({ maxBytes: 1024 * 1024 }),
124+
);
125+
});
126+
97127
it("creates app with managed identity credentials", async () => {
98128
const creds: MSTeamsFederatedCredentials = {
99129
type: "federated",

extensions/msteams/src/sdk.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
// Msteams plugin module implements sdk behavior.
2-
import * as fs from "node:fs";
32
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
3+
import { tryReadSecretFileSync } from "openclaw/plugin-sdk/secret-file-runtime";
44
import { normalizeBotFrameworkServiceUrl } from "./bot-framework-service-url.js";
55
import type { MSTeamsCloudName } from "./cloud.js";
66
import { MSTEAMS_REQUEST_TIMEOUT_MS } from "./request-timeout.js";
77
import type { MSTeamsCredentials, MSTeamsFederatedCredentials } from "./token.js";
88
import { buildOpenClawUserAgentFragment } from "./user-agent.js";
99

10+
// Bound the federated certificate file read so a misconfigured or oversized
11+
// path cannot trigger an unbounded read at credential-load time.
12+
const MSTEAMS_CERTIFICATE_MAX_BYTES = 1024 * 1024;
13+
1014
type MSTeamsHttpServerAdapter =
1115
import("@microsoft/teams.apps/dist/http/adapter.js").IHttpServerAdapter;
1216

@@ -319,13 +323,22 @@ function createFederatedApp(
319323

320324
let privateKey: string;
321325
try {
322-
privateKey = fs.readFileSync(creds.certificatePath, "utf-8");
326+
privateKey =
327+
tryReadSecretFileSync(creds.certificatePath, "MS Teams federated certificate", {
328+
maxBytes: MSTEAMS_CERTIFICATE_MAX_BYTES,
329+
rejectHardlinks: false,
330+
}) ?? "";
323331
} catch (err: unknown) {
324332
const msg = err instanceof Error ? err.message : String(err);
325333
throw new Error(`Failed to read certificate file at '${creds.certificatePath}': ${msg}`, {
326334
cause: err,
327335
});
328336
}
337+
if (!privateKey) {
338+
throw new Error(
339+
`Failed to read certificate file at '${creds.certificatePath}': file is empty or missing`,
340+
);
341+
}
329342

330343
return createCertificateApp(creds, privateKey, App, appOptions);
331344
}

0 commit comments

Comments
 (0)