fix(proxy): reject malformed debug proxy targets

This commit is contained in:
Vincent Koc
2026-05-14 17:45:54 +08:00
parent a47132734b
commit 92524fcf98
3 changed files with 63 additions and 1 deletions
+1
View File
@@ -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.
@@ -71,6 +71,24 @@ async function requestThroughProxy(proxyUrl: string, targetUrl: string): Promise
return data;
}
async function requestRawThroughProxy(proxyUrl: string, request: string): Promise<string> {
const proxy = new URL(proxyUrl);
const socket = new Socket();
let data = "";
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
data += chunk;
});
await new Promise<void>((resolve, reject) => {
socket.once("error", reject);
socket.connect(Number(proxy.port), proxy.hostname, resolve);
});
socket.write(request);
await new Promise<void>((resolve) => socket.once("end", resolve));
socket.destroy();
return data;
}
async function startCanaryOrigin(): Promise<{
requestCount: () => number;
stop: () => Promise<void>;
@@ -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();
}
});
});
+28 -1
View File
@@ -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) {