fix: Provider-supplied OAuth URLs inject Windows cmd.exe via openUrl (#64161)

* fix: harden Windows browser URL opening

Use explorer.exe directly for OAuth/browser launch on Windows so provider-supplied URLs are never parsed through cmd.exe metacharacter rules.

* fix: harden Windows browser URL opening

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
Coy Geek
2026-04-12 08:42:24 -07:00
committed by GitHub
parent 5fde14b844
commit 4938b2cc43
3 changed files with 12 additions and 27 deletions

View File

@@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Windows/onboarding: open provider OAuth and sign-in URLs with `explorer.exe` instead of routing them through `cmd /c start`, so quoted provider URLs cannot break out into host command execution. (#64161) Thanks @coygeek and @vincentkoc.
- OpenAI/Codex OAuth: stop rewriting the upstream authorize URL scopes so new Codex sign-ins do not fail with `invalid_scope` before returning an authorization code. (#64713) Thanks @fuller-stack-dev.
- Audio transcription: disable pinned DNS only for OpenAI-compatible multipart requests, while still validating hostnames, so OpenAI, Groq, and Mistral transcription works again without weakening other request paths. (#64766) Thanks @GodsBoy.
- macOS/Talk Mode: after granting microphone permission on first enable, continue starting Talk Mode instead of requiring a second toggle. (#62459) Thanks @ggarber.

View File

@@ -44,7 +44,7 @@ afterEach(() => {
});
describe("openUrl", () => {
it("quotes URLs on win32 so '&' is not treated as cmd separator", async () => {
it("passes OAuth URLs to explorer.exe on win32 without cmd parsing", async () => {
vi.stubEnv("VITEST", "");
vi.stubEnv("NODE_ENV", "");
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
@@ -59,23 +59,20 @@ describe("openUrl", () => {
expect(mocks.runCommandWithTimeout).toHaveBeenCalledTimes(1);
const [argv, options] = mocks.runCommandWithTimeout.mock.calls[0] ?? [];
expect(argv?.slice(0, 4)).toEqual(["cmd", "/c", "start", '""']);
expect(argv?.at(-1)).toBe(`"${url}"`);
expect(options).toMatchObject({
timeoutMs: 5_000,
windowsVerbatimArguments: true,
});
expect(argv).toEqual(["explorer.exe", url]);
expect(options).toMatchObject({ timeoutMs: 5_000 });
expect(options?.windowsVerbatimArguments).toBeUndefined();
platformSpy.mockRestore();
});
});
describe("resolveBrowserOpenCommand", () => {
it("marks win32 commands as quoteUrl=true", async () => {
it("uses explorer.exe on win32", async () => {
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
const resolved = await resolveBrowserOpenCommand();
expect(resolved.argv).toEqual(["cmd", "/c", "start", ""]);
expect(resolved.quoteUrl).toBe(true);
expect(resolved.argv).toEqual(["explorer.exe"]);
expect(resolved.command).toBe("explorer.exe");
platformSpy.mockRestore();
});
});

View File

@@ -6,7 +6,6 @@ export type BrowserOpenCommand = {
argv: string[] | null;
reason?: string;
command?: string;
quoteUrl?: boolean;
};
export type BrowserOpenSupport = {
@@ -36,9 +35,8 @@ export async function resolveBrowserOpenCommand(): Promise<BrowserOpenCommand> {
if (platform === "win32") {
return {
argv: ["cmd", "/c", "start", ""],
command: "cmd",
quoteUrl: true,
argv: ["explorer.exe"],
command: "explorer.exe",
};
}
@@ -86,21 +84,10 @@ export async function openUrl(url: string): Promise<boolean> {
if (!resolved.argv) {
return false;
}
const quoteUrl = resolved.quoteUrl === true;
const command = [...resolved.argv];
if (quoteUrl) {
if (command.at(-1) === "") {
command[command.length - 1] = '""';
}
command.push(`"${url}"`);
} else {
command.push(url);
}
command.push(url);
try {
await runCommandWithTimeout(command, {
timeoutMs: 5_000,
windowsVerbatimArguments: quoteUrl,
});
await runCommandWithTimeout(command, { timeoutMs: 5_000 });
return true;
} catch {
return false;