mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-26 17:32:16 +00:00
fix(browser): enforce node browser proxy allowProfiles
This commit is contained in:
@@ -15,7 +15,7 @@ const dispatcherMocks = vi.hoisted(() => ({
|
||||
const configMocks = vi.hoisted(() => ({
|
||||
loadConfig: vi.fn(() => ({
|
||||
browser: {},
|
||||
nodeHost: { browserProxy: { enabled: true } },
|
||||
nodeHost: { browserProxy: { enabled: true, allowProfiles: [] as string[] } },
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -50,7 +50,7 @@ describe("runBrowserProxyCommand", () => {
|
||||
controlServiceMocks.startBrowserControlServiceFromConfig.mockReset().mockResolvedValue(true);
|
||||
configMocks.loadConfig.mockReset().mockReturnValue({
|
||||
browser: {},
|
||||
nodeHost: { browserProxy: { enabled: true } },
|
||||
nodeHost: { browserProxy: { enabled: true, allowProfiles: [] as string[] } },
|
||||
});
|
||||
browserConfigMocks.resolveBrowserConfig.mockReset().mockReturnValue({
|
||||
enabled: true,
|
||||
@@ -59,7 +59,7 @@ describe("runBrowserProxyCommand", () => {
|
||||
({ runBrowserProxyCommand } = await import("./invoke-browser.js"));
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: {},
|
||||
nodeHost: { browserProxy: { enabled: true } },
|
||||
nodeHost: { browserProxy: { enabled: true, allowProfiles: [] as string[] } },
|
||||
});
|
||||
browserConfigMocks.resolveBrowserConfig.mockReturnValue({
|
||||
enabled: true,
|
||||
@@ -183,4 +183,134 @@ describe("runBrowserProxyCommand", () => {
|
||||
),
|
||||
).rejects.toThrow("tab not found");
|
||||
});
|
||||
|
||||
it("rejects unauthorized query.profile when allowProfiles is configured", async () => {
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: {},
|
||||
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
|
||||
});
|
||||
|
||||
await expect(
|
||||
runBrowserProxyCommand(
|
||||
JSON.stringify({
|
||||
method: "GET",
|
||||
path: "/snapshot",
|
||||
query: { profile: "user" },
|
||||
timeoutMs: 50,
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("INVALID_REQUEST: browser profile not allowed");
|
||||
expect(dispatcherMocks.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects unauthorized body.profile when allowProfiles is configured", async () => {
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: {},
|
||||
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
|
||||
});
|
||||
|
||||
await expect(
|
||||
runBrowserProxyCommand(
|
||||
JSON.stringify({
|
||||
method: "POST",
|
||||
path: "/stop",
|
||||
body: { profile: "user" },
|
||||
timeoutMs: 50,
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow("INVALID_REQUEST: browser profile not allowed");
|
||||
expect(dispatcherMocks.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects persistent profile creation when allowProfiles is configured", async () => {
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: {},
|
||||
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
|
||||
});
|
||||
|
||||
await expect(
|
||||
runBrowserProxyCommand(
|
||||
JSON.stringify({
|
||||
method: "POST",
|
||||
path: "/profiles/create",
|
||||
body: { name: "poc", cdpUrl: "http://127.0.0.1:9222" },
|
||||
timeoutMs: 50,
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow(
|
||||
"INVALID_REQUEST: browser.proxy cannot create or delete persistent browser profiles when allowProfiles is configured",
|
||||
);
|
||||
expect(dispatcherMocks.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects persistent profile deletion when allowProfiles is configured", async () => {
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: {},
|
||||
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
|
||||
});
|
||||
|
||||
await expect(
|
||||
runBrowserProxyCommand(
|
||||
JSON.stringify({
|
||||
method: "DELETE",
|
||||
path: "/profiles/poc",
|
||||
timeoutMs: 50,
|
||||
}),
|
||||
),
|
||||
).rejects.toThrow(
|
||||
"INVALID_REQUEST: browser.proxy cannot create or delete persistent browser profiles when allowProfiles is configured",
|
||||
);
|
||||
expect(dispatcherMocks.dispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("canonicalizes an allowlisted body profile into the dispatched query", async () => {
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: {},
|
||||
nodeHost: { browserProxy: { enabled: true, allowProfiles: ["openclaw"] } },
|
||||
});
|
||||
dispatcherMocks.dispatch.mockResolvedValue({
|
||||
status: 200,
|
||||
body: { ok: true },
|
||||
});
|
||||
|
||||
await runBrowserProxyCommand(
|
||||
JSON.stringify({
|
||||
method: "POST",
|
||||
path: "/stop",
|
||||
body: { profile: "openclaw" },
|
||||
timeoutMs: 50,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(dispatcherMocks.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
path: "/stop",
|
||||
query: { profile: "openclaw" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves legacy proxy behavior when allowProfiles is empty", async () => {
|
||||
dispatcherMocks.dispatch.mockResolvedValue({
|
||||
status: 200,
|
||||
body: { ok: true },
|
||||
});
|
||||
|
||||
await runBrowserProxyCommand(
|
||||
JSON.stringify({
|
||||
method: "POST",
|
||||
path: "/profiles/create",
|
||||
body: { name: "poc", cdpUrl: "http://127.0.0.1:9222" },
|
||||
timeoutMs: 50,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(dispatcherMocks.dispatch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
path: "/profiles/create",
|
||||
body: { name: "poc", cdpUrl: "http://127.0.0.1:9222" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,11 @@ import {
|
||||
createBrowserControlContext,
|
||||
startBrowserControlServiceFromConfig,
|
||||
} from "../browser/control-service.js";
|
||||
import {
|
||||
isPersistentBrowserProfileMutation,
|
||||
normalizeBrowserRequestPath,
|
||||
resolveRequestedBrowserProfile,
|
||||
} from "../browser/request-policy.js";
|
||||
import { createBrowserRouteDispatcher } from "../browser/routes/dispatcher.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { detectMime } from "../media/mime.js";
|
||||
@@ -221,10 +226,23 @@ export async function runBrowserProxyCommand(paramsJSON?: string | null): Promis
|
||||
await ensureBrowserControlService();
|
||||
const cfg = loadConfig();
|
||||
const resolved = resolveBrowserConfig(cfg.browser, cfg);
|
||||
const requestedProfile = typeof params.profile === "string" ? params.profile.trim() : "";
|
||||
const method = typeof params.method === "string" ? params.method.toUpperCase() : "GET";
|
||||
const path = normalizeBrowserRequestPath(pathValue);
|
||||
const body = params.body;
|
||||
const requestedProfile =
|
||||
resolveRequestedBrowserProfile({
|
||||
query: params.query,
|
||||
body,
|
||||
profile: params.profile,
|
||||
}) ?? "";
|
||||
const allowedProfiles = proxyConfig.allowProfiles;
|
||||
if (allowedProfiles.length > 0) {
|
||||
if (pathValue !== "/profiles") {
|
||||
if (isPersistentBrowserProfileMutation(method, path)) {
|
||||
throw new Error(
|
||||
"INVALID_REQUEST: browser.proxy cannot create or delete persistent browser profiles when allowProfiles is configured",
|
||||
);
|
||||
}
|
||||
if (path !== "/profiles") {
|
||||
const profileToCheck = requestedProfile || resolved.defaultProfile;
|
||||
if (!isProfileAllowed({ allowProfiles: allowedProfiles, profile: profileToCheck })) {
|
||||
throw new Error("INVALID_REQUEST: browser profile not allowed");
|
||||
@@ -236,14 +254,8 @@ export async function runBrowserProxyCommand(paramsJSON?: string | null): Promis
|
||||
}
|
||||
}
|
||||
|
||||
const method = typeof params.method === "string" ? params.method.toUpperCase() : "GET";
|
||||
const path = pathValue.startsWith("/") ? pathValue : `/${pathValue}`;
|
||||
const body = params.body;
|
||||
const timeoutMs = resolveBrowserProxyTimeout(params.timeoutMs);
|
||||
const query: Record<string, unknown> = {};
|
||||
if (requestedProfile) {
|
||||
query.profile = requestedProfile;
|
||||
}
|
||||
const rawQuery = params.query ?? {};
|
||||
for (const [key, value] of Object.entries(rawQuery)) {
|
||||
if (value === undefined || value === null) {
|
||||
@@ -251,6 +263,9 @@ export async function runBrowserProxyCommand(paramsJSON?: string | null): Promis
|
||||
}
|
||||
query[key] = typeof value === "string" ? value : String(value);
|
||||
}
|
||||
if (requestedProfile) {
|
||||
query.profile = requestedProfile;
|
||||
}
|
||||
|
||||
const dispatcher = createBrowserRouteDispatcher(createBrowserControlContext());
|
||||
let response;
|
||||
|
||||
Reference in New Issue
Block a user