mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 06:31:24 +00:00
fix(security): harden browser SSRF defaults and migrate legacy key
This commit is contained in:
47
src/agents/tools/web-search.redirect.test.ts
Normal file
47
src/agents/tools/web-search.redirect.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
|
||||
fetchWithSsrFGuardMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../infra/net/fetch-guard.js", () => ({
|
||||
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
|
||||
}));
|
||||
|
||||
import { __testing } from "./web-search.js";
|
||||
|
||||
describe("web_search redirect resolution hardening", () => {
|
||||
const { resolveRedirectUrl } = __testing;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchWithSsrFGuardMock.mockReset();
|
||||
});
|
||||
|
||||
it("resolves redirects via SSRF-guarded HEAD requests", async () => {
|
||||
const release = vi.fn(async () => {});
|
||||
fetchWithSsrFGuardMock.mockResolvedValue({
|
||||
response: new Response(null, { status: 200 }),
|
||||
finalUrl: "https://example.com/final",
|
||||
release,
|
||||
});
|
||||
|
||||
const resolved = await resolveRedirectUrl("https://example.com/start");
|
||||
expect(resolved).toBe("https://example.com/final");
|
||||
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://example.com/start",
|
||||
timeoutMs: 5000,
|
||||
init: { method: "HEAD" },
|
||||
policy: { dangerouslyAllowPrivateNetwork: true },
|
||||
}),
|
||||
);
|
||||
expect(release).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("falls back to the original URL when guarded resolution fails", async () => {
|
||||
fetchWithSsrFGuardMock.mockRejectedValue(new Error("blocked"));
|
||||
await expect(resolveRedirectUrl("https://example.com/start")).resolves.toBe(
|
||||
"https://example.com/start",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { formatCliCommand } from "../../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import { wrapWebContent } from "../../security/external-content.js";
|
||||
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
|
||||
@@ -42,6 +43,7 @@ const KIMI_WEB_SEARCH_TOOL = {
|
||||
const SEARCH_CACHE = new Map<string, CacheEntry<Record<string, unknown>>>();
|
||||
const BRAVE_FRESHNESS_SHORTCUTS = new Set(["pd", "pw", "pm", "py"]);
|
||||
const BRAVE_FRESHNESS_RANGE = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/;
|
||||
const TRUSTED_NETWORK_SSRF_POLICY = { dangerouslyAllowPrivateNetwork: true } as const;
|
||||
|
||||
const WebSearchSchema = Type.Object({
|
||||
query: Type.String({ description: "Search query string." }),
|
||||
@@ -681,12 +683,17 @@ const REDIRECT_TIMEOUT_MS = 5000;
|
||||
*/
|
||||
async function resolveRedirectUrl(url: string): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "HEAD",
|
||||
redirect: "follow",
|
||||
signal: withTimeout(undefined, REDIRECT_TIMEOUT_MS),
|
||||
const { finalUrl, release } = await fetchWithSsrFGuard({
|
||||
url,
|
||||
init: { method: "HEAD" },
|
||||
timeoutMs: REDIRECT_TIMEOUT_MS,
|
||||
policy: TRUSTED_NETWORK_SSRF_POLICY,
|
||||
});
|
||||
return res.url || url;
|
||||
try {
|
||||
return finalUrl || url;
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
} catch {
|
||||
return url;
|
||||
}
|
||||
@@ -1345,4 +1352,5 @@ export const __testing = {
|
||||
resolveKimiModel,
|
||||
resolveKimiBaseUrl,
|
||||
extractKimiCitations,
|
||||
resolveRedirectUrl,
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user