mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 14:28:27 +00:00
fix(secrets): enforce file provider read timeouts
This commit is contained in:
committed by
Peter Steinberger
parent
67e9554645
commit
86622ebea9
@@ -1,7 +1,7 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveSecretRefString, resolveSecretRefValue } from "./resolve.js";
|
import { resolveSecretRefString, resolveSecretRefValue } from "./resolve.js";
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ describe("secret ref resolver", () => {
|
|||||||
const cleanupRoots: string[] = [];
|
const cleanupRoots: string[] = [];
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
while (cleanupRoots.length > 0) {
|
while (cleanupRoots.length > 0) {
|
||||||
const root = cleanupRoots.pop();
|
const root = cleanupRoots.pop();
|
||||||
if (!root) {
|
if (!root) {
|
||||||
@@ -280,6 +281,56 @@ describe("secret ref resolver", () => {
|
|||||||
expect(value).toBe("raw-token-value");
|
expect(value).toBe("raw-token-value");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("times out file provider reads when timeoutMs elapses", async () => {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-secrets-resolve-timeout-"));
|
||||||
|
cleanupRoots.push(root);
|
||||||
|
const filePath = path.join(root, "secrets.json");
|
||||||
|
await writeSecureFile(
|
||||||
|
filePath,
|
||||||
|
JSON.stringify({
|
||||||
|
providers: {
|
||||||
|
openai: {
|
||||||
|
apiKey: "sk-file-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const originalReadFile = fs.readFile.bind(fs);
|
||||||
|
vi.spyOn(fs, "readFile").mockImplementation(((
|
||||||
|
targetPath: Parameters<typeof fs.readFile>[0],
|
||||||
|
options?: Parameters<typeof fs.readFile>[1],
|
||||||
|
) => {
|
||||||
|
if (typeof targetPath === "string" && targetPath === filePath) {
|
||||||
|
return new Promise<Buffer>(() => {});
|
||||||
|
}
|
||||||
|
return originalReadFile(targetPath, options);
|
||||||
|
}) as typeof fs.readFile);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
resolveSecretRefString(
|
||||||
|
{ source: "file", provider: "filemain", id: "/providers/openai/apiKey" },
|
||||||
|
{
|
||||||
|
config: {
|
||||||
|
secrets: {
|
||||||
|
providers: {
|
||||||
|
filemain: {
|
||||||
|
source: "file",
|
||||||
|
path: filePath,
|
||||||
|
mode: "jsonPointer",
|
||||||
|
timeoutMs: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
).rejects.toThrow('File provider "filemain" timed out');
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects misconfigured provider source mismatches", async () => {
|
it("rejects misconfigured provider source mismatches", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
resolveSecretRefValue(
|
resolveSecretRefValue(
|
||||||
|
|||||||
@@ -188,11 +188,20 @@ async function readFileProviderPayload(params: {
|
|||||||
DEFAULT_FILE_TIMEOUT_MS,
|
DEFAULT_FILE_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
const maxBytes = normalizePositiveInt(params.providerConfig.maxBytes, DEFAULT_FILE_MAX_BYTES);
|
const maxBytes = normalizePositiveInt(params.providerConfig.maxBytes, DEFAULT_FILE_MAX_BYTES);
|
||||||
const timeoutHandle = setTimeout(() => {
|
const abortController = new AbortController();
|
||||||
// noop marker to keep timeout behavior explicit and deterministic
|
const timeoutErrorMessage = `File provider "${params.providerName}" timed out after ${timeoutMs}ms.`;
|
||||||
}, timeoutMs);
|
let timeoutHandle: NodeJS.Timeout | null = null;
|
||||||
|
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
||||||
|
timeoutHandle = setTimeout(() => {
|
||||||
|
abortController.abort();
|
||||||
|
reject(new Error(timeoutErrorMessage));
|
||||||
|
}, timeoutMs);
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const payload = await fs.readFile(filePath);
|
const payload = await Promise.race([
|
||||||
|
fs.readFile(filePath, { signal: abortController.signal }),
|
||||||
|
timeoutPromise,
|
||||||
|
]);
|
||||||
if (payload.byteLength > maxBytes) {
|
if (payload.byteLength > maxBytes) {
|
||||||
throw new Error(`File provider "${params.providerName}" exceeded maxBytes (${maxBytes}).`);
|
throw new Error(`File provider "${params.providerName}" exceeded maxBytes (${maxBytes}).`);
|
||||||
}
|
}
|
||||||
@@ -205,8 +214,15 @@ async function readFileProviderPayload(params: {
|
|||||||
throw new Error(`File provider "${params.providerName}" payload is not a JSON object.`);
|
throw new Error(`File provider "${params.providerName}" payload is not a JSON object.`);
|
||||||
}
|
}
|
||||||
return parsed;
|
return parsed;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
throw new Error(timeoutErrorMessage, { cause: error });
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
clearTimeout(timeoutHandle);
|
if (timeoutHandle) {
|
||||||
|
clearTimeout(timeoutHandle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user