fix(browser): land PR #11880 decodeURIComponent guardrails

Guard malformed percent-encoding in relay target routes and browser dispatcher params, add regression tests, and update changelog.
Landed from contributor @Yida-Dev (PR #11880).

Co-authored-by: Yida-Dev <reyifeijun@gmail.com>
This commit is contained in:
Peter Steinberger
2026-02-26 14:37:33 +00:00
parent 62a248eb99
commit 79659b2b14
5 changed files with 53 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
- Browser/Extension relay shutdown: flush pending extension-request timers/rejections during relay `stop()` before socket/server teardown so in-flight extension waits do not survive shutdown windows. Landed from contributor PR #24142 by @kevinWangSheng.
- Browser/Chrome extension handshake: bind relay WS message handling before `onopen` and add non-blocking `connect.challenge` response handling for gateway-style handshake frames, avoiding stuck `…` badge states when challenge frames arrive immediately on connect. Landed from contributor PR #22571 by @pandego. (#22553)
- Browser/Extension relay init: dedupe concurrent same-port relay startup with shared in-flight initialization promises so callers await one startup lifecycle and receive consistent success/failure results. Landed from contributor PR #21277 by @HOYALIM. (Related #20688)
- Browser/Route decode hardening: guard malformed percent-encoding in relay target action routes and browser route-param decoding so crafted `%` paths return `400` instead of crashing/unhandled URI decode failures. Landed from contributor PR #11880 by @Yida-Dev.
- Auth/Auth profiles: normalize `auth-profiles.json` alias fields (`mode -> type`, `apiKey -> key`) before credential validation so entries copied from `openclaw.json` auth examples are no longer silently dropped. (#26950) thanks @byungsker.
- Cron/Hooks isolated routing: preserve canonical `agent:*` session keys in isolated runs so already-qualified keys are not double-prefixed (for example `agent:main:main` no longer becomes `agent:main:agent:main:main`). Landed from contributor PR #27333 by @MaheshBhushan. (#27289, #27282)
- iOS/Talk mode: stop injecting the voice directive hint into iOS Talk prompts and remove the Voice Directive Hint setting, reducing model bias toward tool-style TTS directives and keeping relay responses text-first by default. (#27543) thanks @ngutman.

View File

@@ -208,6 +208,18 @@ describe("chrome extension relay server", () => {
expect(err.message).toContain("401");
});
it("returns 400 for malformed percent-encoding in target action routes", async () => {
const port = await getFreePort();
cdpUrl = `http://127.0.0.1:${port}`;
await ensureChromeExtensionRelayServer({ cdpUrl });
const res = await fetch(`${cdpUrl}/json/activate/%E0%A4%A`, {
headers: relayAuthHeaders(cdpUrl),
});
expect(res.status).toBe(400);
expect(await res.text()).toContain("invalid targetId encoding");
});
it("deduplicates concurrent relay starts for the same requested port", async () => {
const port = await getFreePort();
cdpUrl = `http://127.0.0.1:${port}`;

View File

@@ -476,7 +476,14 @@ export async function ensureChromeExtensionRelayServer(opts: {
if (!match || (req.method !== "GET" && req.method !== "PUT")) {
return false;
}
const targetId = decodeURIComponent(match[1] ?? "").trim();
let targetId = "";
try {
targetId = decodeURIComponent(match[1] ?? "").trim();
} catch {
res.writeHead(400);
res.end("invalid targetId encoding");
return true;
}
if (!targetId) {
res.writeHead(400);
res.end("targetId required");

View File

@@ -23,6 +23,15 @@ vi.mock("./index.js", () => {
res.json({ ok: true });
},
);
app.get(
"/echo/:id",
async (
req: { params?: Record<string, string> },
res: { json: (body: unknown) => void },
) => {
res.json({ id: req.params?.id ?? null });
},
);
},
};
});
@@ -46,4 +55,19 @@ describe("browser route dispatcher (abort)", () => {
body: { error: expect.stringContaining("timed out") },
});
});
it("returns 400 for malformed percent-encoding in route params", async () => {
const { createBrowserRouteDispatcher } = await import("./dispatcher.js");
const dispatcher = createBrowserRouteDispatcher({} as BrowserRouteContext);
await expect(
dispatcher.dispatch({
method: "GET",
path: "/echo/%E0%A4%A",
}),
).resolves.toMatchObject({
status: 400,
body: { error: expect.stringContaining("invalid path parameter encoding") },
});
});
});

View File

@@ -87,7 +87,14 @@ export function createBrowserRouteDispatcher(ctx: BrowserRouteContext) {
for (const [idx, name] of match.paramNames.entries()) {
const value = exec[idx + 1];
if (typeof value === "string") {
params[name] = decodeURIComponent(value);
try {
params[name] = decodeURIComponent(value);
} catch {
return {
status: 400,
body: { error: `invalid path parameter encoding: ${name}` },
};
}
}
}
}