Gateway: block profile mutations via browser.request (#43800)

* Gateway: block profile mutations via browser.request

* Changelog: note GHSA-vmhq browser request fix

* Gateway: normalize browser.request profile guard paths
This commit is contained in:
Vincent Koc 2026-03-12 04:21:03 -04:00 committed by GitHub
parent 46a332385d
commit f37815b323
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 70 additions and 0 deletions

View File

@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
- Security/host env: block inherited `GIT_EXEC_PATH` from sanitized host exec environments so Git helper resolution cannot be steered by host environment state. (`GHSA-jf5v-pqgw-gm5m`)(#43685) Thanks @zpbrent and @vincentkoc.
- Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via `session_status`. (`GHSA-wcxr-59v9-rxr8`)(#43754) Thanks @tdjackey and @vincentkoc.
- Models/secrets: enforce source-managed SecretRef markers in generated `models.json` so runtime-resolved provider secrets are not persisted when runtime projection is skipped. (#43759) Thanks @joshavant.
- Security/browser.request: block persistent browser profile create/delete routes from write-scoped `browser.request` so callers can no longer persist admin-only browser profile changes through the browser control surface. (`GHSA-vmhq-cqm9-6p7q`)(#43800) Thanks @tdjackey and @vincentkoc.
- Security/agent: reject public spawned-run lineage fields and keep workspace inheritance on the internal spawned-session path so external `agent` callers can no longer override the gateway workspace boundary. (`GHSA-2rqg-gjgv-84jm`)(#43801) Thanks @tdjackey and @vincentkoc.
- Security/exec allowlist: preserve POSIX case sensitivity and keep `?` within a single path segment so exact-looking allowlist patterns no longer overmatch executables across case or directory boundaries. (`GHSA-f8r2-vg7x-gh8m`)(#43798) Thanks @zpbrent and @vincentkoc.

View File

@ -100,4 +100,42 @@ describe("browser.request profile selection", () => {
}),
);
});
it.each([
{
method: "POST",
path: "/profiles/create",
body: { name: "poc", cdpUrl: "http://10.0.0.42:9222" },
},
{
method: "DELETE",
path: "/profiles/poc",
body: undefined,
},
{
method: "POST",
path: "profiles/create",
body: { name: "poc", cdpUrl: "http://10.0.0.42:9222" },
},
{
method: "DELETE",
path: "profiles/poc",
body: undefined,
},
])("blocks persistent profile mutations for $method $path", async ({ method, path, body }) => {
const { respond, nodeRegistry } = await runBrowserRequest({
method,
path,
body,
});
expect(nodeRegistry.invoke).not.toHaveBeenCalled();
expect(respond).toHaveBeenCalledWith(
false,
undefined,
expect.objectContaining({
message: "browser.request cannot create or delete persistent browser profiles",
}),
);
});
});

View File

@ -20,6 +20,26 @@ type BrowserRequestParams = {
timeoutMs?: number;
};
function normalizeBrowserRequestPath(value: string): string {
const trimmed = value.trim();
if (!trimmed) {
return trimmed;
}
const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
if (withLeadingSlash.length <= 1) {
return withLeadingSlash;
}
return withLeadingSlash.replace(/\/+$/, "");
}
function isPersistentBrowserProfileMutation(method: string, path: string): boolean {
const normalizedPath = normalizeBrowserRequestPath(path);
if (method === "POST" && normalizedPath === "/profiles/create") {
return true;
}
return method === "DELETE" && /^\/profiles\/[^/]+$/.test(normalizedPath);
}
function resolveRequestedProfile(params: {
query?: Record<string, unknown>;
body?: unknown;
@ -167,6 +187,17 @@ export const browserHandlers: GatewayRequestHandlers = {
);
return;
}
if (isPersistentBrowserProfileMutation(methodRaw, path)) {
respond(
false,
undefined,
errorShape(
ErrorCodes.INVALID_REQUEST,
"browser.request cannot create or delete persistent browser profiles",
),
);
return;
}
const cfg = loadConfig();
let nodeTarget: NodeSession | null = null;