diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b88c700ae12..f82cf606f75e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai - Browser tool: treat malformed node proxy `payloadJSON` responses as browser proxy failures instead of leaking raw JSON parser errors. - Gateway HTTP: match models, session kill, and session history route paths without trusting malformed Host headers, avoiding pre-auth 500s on those endpoints. - Google Meet/Codex: report malformed node proxy `payloadJSON` responses with plugin-owned errors instead of leaking raw JSON parser failures. +- Debug proxy: reject malformed relative-form proxy targets with a controlled 400 response instead of letting URL parsing escape the request handler. - Models config/auth: stop inferring provider env-var markers from broad `^[A-Z_][A-Z0-9_]*$` strings, and resolve config-backed provider `apiKey` values only through structured env SecretRefs (`secrets.providers[id]` / `secrets.defaults`), so unrelated env vars cannot accidentally become provider credentials. Thanks @sallyom. - Media fetch: skip allocating and buffering the response body for bodyless media responses (HEAD probes and 204-style empty bodies), avoiding wasted heap on streams that carry no payload. Thanks @shakkernerd. - CLI/onboarding: forward provider-specific auth flags (e.g. `--openai-api-key`) through the onboarding wizard so they reach provider auth methods via `ctx.opts`, letting `--openai-api-key "$OPENAI_API_KEY"` skip the redundant "use existing env var?" prompt in non-interactive harnesses. (#81669) Thanks @sjf. diff --git a/src/proxy-capture/proxy-server.managed-proxy.test.ts b/src/proxy-capture/proxy-server.managed-proxy.test.ts index d25b71091697..7dabfb79690e 100644 --- a/src/proxy-capture/proxy-server.managed-proxy.test.ts +++ b/src/proxy-capture/proxy-server.managed-proxy.test.ts @@ -71,6 +71,24 @@ async function requestThroughProxy(proxyUrl: string, targetUrl: string): Promise return data; } +async function requestRawThroughProxy(proxyUrl: string, request: string): Promise { + const proxy = new URL(proxyUrl); + const socket = new Socket(); + let data = ""; + socket.setEncoding("utf8"); + socket.on("data", (chunk) => { + data += chunk; + }); + await new Promise((resolve, reject) => { + socket.once("error", reject); + socket.connect(Number(proxy.port), proxy.hostname, resolve); + }); + socket.write(request); + await new Promise((resolve) => socket.once("end", resolve)); + socket.destroy(); + return data; +} + async function startCanaryOrigin(): Promise<{ requestCount: () => number; stop: () => Promise; @@ -188,4 +206,20 @@ describe("debug proxy managed-proxy direct upstream policy", () => { await origin.stop(); } }); + + it("rejects malformed relative-form HTTP proxy targets before upstream handling", async () => { + const server = await startDebugProxyServer({ settings: await makeSettings() }); + try { + const response = await requestRawThroughProxy( + server.proxyUrl, + "GET /capture HTTP/1.1\r\nHost: [\r\nConnection: close\r\n\r\n", + ); + + expect(response).toContain("400 Bad Request"); + expect(response).toContain("Connection: close"); + expect(response).toContain("Invalid proxy target URL"); + } finally { + await server.stop(); + } + }); }); diff --git a/src/proxy-capture/proxy-server.ts b/src/proxy-capture/proxy-server.ts index 3ea799b47104..c702f6b77c08 100644 --- a/src/proxy-capture/proxy-server.ts +++ b/src/proxy-capture/proxy-server.ts @@ -98,7 +98,34 @@ export async function startDebugProxyServer(params: { const server = createServer(async (req: IncomingMessage, res: ServerResponse) => { const flowId = randomUUID(); - const target = normalizeTargetUrl(req); + let target: URL; + try { + target = normalizeTargetUrl(req); + } catch (error) { + const message = "Invalid proxy target URL"; + store.recordEvent({ + sessionId: params.settings.sessionId, + ts: Date.now(), + sourceScope: "openclaw", + sourceProcess: params.settings.sourceProcess, + protocol: "http", + direction: "local", + kind: "error", + flowId, + method: req.method, + host: req.headers.host, + path: req.url ?? "", + errorText: error instanceof Error ? error.message : String(error), + }); + const responseBody = `${message}\n`; + res.writeHead(400, { + Connection: "close", + "Content-Type": "text/plain; charset=utf-8", + "Content-Length": Buffer.byteLength(responseBody), + }); + res.end(responseBody); + return; + } try { assertDebugProxyDirectUpstreamAllowed(); } catch (error) {