fix(gateway): reject malformed request targets (#82686)

* fix(gateway): reject malformed request targets

* fix(gateway): document malformed request target rejection
This commit is contained in:
Agustin Rivera
2026-05-16 13:25:49 -07:00
committed by GitHub
parent 55edadf86f
commit f7977fb102
5 changed files with 133 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import type { Socket } from "node:net";
import { connect, type Socket } from "node:net";
import type { Duplex } from "node:stream";
import { describe, expect, test } from "vitest";
import { WebSocket, WebSocketServer } from "ws";
@@ -172,6 +172,50 @@ async function expectWsConnected(url: string, headers?: Record<string, string>):
});
}
async function sendRawHttpRequest(params: {
host: string;
port: number;
requestTarget: string;
headers?: readonly string[];
}): Promise<string> {
return new Promise<string>((resolve, reject) => {
const socket = connect({ host: params.host, port: params.port }, () => {
const headers = params.headers ?? ["Host: localhost", "Connection: close"];
socket.write([`GET ${params.requestTarget} HTTP/1.1`, ...headers, "", ""].join("\r\n"));
});
let response = "";
let settled = false;
const finish = (fn: () => void) => {
if (settled) {
return;
}
settled = true;
socket.setTimeout(0);
fn();
};
socket.setEncoding("utf8");
socket.setTimeout(WS_REJECT_TIMEOUT_MS, () => {
const error = new Error("timeout");
finish(() => {
socket.destroy(error);
reject(error);
});
});
socket.on("data", (chunk) => {
response += chunk;
});
socket.once("end", () => {
finish(() => resolve(response));
});
socket.once("close", () => {
finish(() => resolve(response));
});
socket.once("error", (err) => {
finish(() => reject(err));
});
});
}
function makeWsClient(params: {
connId: string;
clientIp: string;
@@ -408,6 +452,53 @@ describe("gateway plugin node capability auth", () => {
}, "openclaw-canvas-auth-test-");
}, 60_000);
test("rejects malformed raw HTTP request targets without disrupting gateway", async () => {
await withCanvasGatewayHarness({
resolvedAuth: tokenResolvedAuth,
handleHttpRequest: allowCanvasHostHttp,
run: async ({ listener }) => {
for (const requestTarget of ["//", "///", "//${jndi:ldap://example}.action"]) {
const response = await sendRawHttpRequest({
host: "127.0.0.1",
port: listener.port,
requestTarget,
});
expect(response).toMatch(/^HTTP\/1\.1 401 /);
}
const res = await fetchCanvas(`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`);
expect(res.status).toBe(401);
},
});
}, 60_000);
test("rejects malformed raw WebSocket upgrade targets without disrupting gateway", async () => {
await withCanvasGatewayHarness({
resolvedAuth: tokenResolvedAuth,
handleHttpRequest: allowCanvasHostHttp,
run: async ({ listener }) => {
for (const requestTarget of ["//", "///", "//${jndi:ldap://example}.action"]) {
const response = await sendRawHttpRequest({
host: "127.0.0.1",
port: listener.port,
requestTarget,
headers: [
"Host: localhost",
"Upgrade: websocket",
"Connection: Upgrade",
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version: 13",
],
});
expect(response).toMatch(/^HTTP\/1\.1 401 /);
}
const res = await fetchCanvas(`http://127.0.0.1:${listener.port}${CANVAS_HOST_PATH}/`);
expect(res.status).toBe(401);
},
});
}, 60_000);
test("denies canvas auth when trusted proxy omits forwarded client headers", async () => {
await withLoopbackTrustedProxy(async () => {
await withCanvasGatewayHarness({