Skip to content

Commit 79659b2

Browse files
steipeteYida-Dev
andcommitted
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 <[email protected]>
1 parent 62a248e commit 79659b2

File tree

5 files changed

+53
-2
lines changed

5 files changed

+53
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Docs: https://docs.openclaw.ai
1818
- 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.
1919
- 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)
2020
- 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)
21+
- 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.
2122
- 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.
2223
- 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)
2324
- 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.

src/browser/extension-relay.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,18 @@ describe("chrome extension relay server", () => {
208208
expect(err.message).toContain("401");
209209
});
210210

211+
it("returns 400 for malformed percent-encoding in target action routes", async () => {
212+
const port = await getFreePort();
213+
cdpUrl = `http://127.0.0.1:${port}`;
214+
await ensureChromeExtensionRelayServer({ cdpUrl });
215+
216+
const res = await fetch(`${cdpUrl}/json/activate/%E0%A4%A`, {
217+
headers: relayAuthHeaders(cdpUrl),
218+
});
219+
expect(res.status).toBe(400);
220+
expect(await res.text()).toContain("invalid targetId encoding");
221+
});
222+
211223
it("deduplicates concurrent relay starts for the same requested port", async () => {
212224
const port = await getFreePort();
213225
cdpUrl = `http://127.0.0.1:${port}`;

src/browser/extension-relay.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -476,7 +476,14 @@ export async function ensureChromeExtensionRelayServer(opts: {
476476
if (!match || (req.method !== "GET" && req.method !== "PUT")) {
477477
return false;
478478
}
479-
const targetId = decodeURIComponent(match[1] ?? "").trim();
479+
let targetId = "";
480+
try {
481+
targetId = decodeURIComponent(match[1] ?? "").trim();
482+
} catch {
483+
res.writeHead(400);
484+
res.end("invalid targetId encoding");
485+
return true;
486+
}
480487
if (!targetId) {
481488
res.writeHead(400);
482489
res.end("targetId required");

src/browser/routes/dispatcher.abort.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@ vi.mock("./index.js", () => {
2323
res.json({ ok: true });
2424
},
2525
);
26+
app.get(
27+
"/echo/:id",
28+
async (
29+
req: { params?: Record<string, string> },
30+
res: { json: (body: unknown) => void },
31+
) => {
32+
res.json({ id: req.params?.id ?? null });
33+
},
34+
);
2635
},
2736
};
2837
});
@@ -46,4 +55,19 @@ describe("browser route dispatcher (abort)", () => {
4655
body: { error: expect.stringContaining("timed out") },
4756
});
4857
});
58+
59+
it("returns 400 for malformed percent-encoding in route params", async () => {
60+
const { createBrowserRouteDispatcher } = await import("./dispatcher.js");
61+
const dispatcher = createBrowserRouteDispatcher({} as BrowserRouteContext);
62+
63+
await expect(
64+
dispatcher.dispatch({
65+
method: "GET",
66+
path: "/echo/%E0%A4%A",
67+
}),
68+
).resolves.toMatchObject({
69+
status: 400,
70+
body: { error: expect.stringContaining("invalid path parameter encoding") },
71+
});
72+
});
4973
});

src/browser/routes/dispatcher.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,14 @@ export function createBrowserRouteDispatcher(ctx: BrowserRouteContext) {
8787
for (const [idx, name] of match.paramNames.entries()) {
8888
const value = exec[idx + 1];
8989
if (typeof value === "string") {
90-
params[name] = decodeURIComponent(value);
90+
try {
91+
params[name] = decodeURIComponent(value);
92+
} catch {
93+
return {
94+
status: 400,
95+
body: { error: `invalid path parameter encoding: ${name}` },
96+
};
97+
}
9198
}
9299
}
93100
}

0 commit comments

Comments
 (0)