Skip to content

Commit 9fb4735

Browse files
thymikeeclaude
andcommitted
fix(proxy): answer only TRACE with 404 and reject credentialed URLs with 400
Node never hands CONNECT or TRACK to the request listener, so the catch-all 404 only reached TRACE, and it misreported credentialed URLs, which Fetch also refuses, as 404. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 5932ced commit 9fb4735

2 files changed

Lines changed: 15 additions & 11 deletions

File tree

‎packages/proxy/src/node-http.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ test('a Host header that cannot form a URL is answered with 400 instead of a dro
5656
expect(served).toEqual({ seenUrl: null, status: 400 });
5757
});
5858

59+
test('a URL carrying credentials is answered with 400 instead of a dropped socket', async () => {
60+
const served = await serveThroughListener(new Socket(), 'user:secret@gateway.example.test');
61+
expect(served).toEqual({ seenUrl: null, status: 400 });
62+
});
63+
5964
test('a method Fetch refuses is answered with 404 instead of a dropped socket', async () => {
6065
const proxy: DaemonProxy = {
6166
instanceId: 'test',

‎packages/proxy/src/node-http.ts‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,17 @@ async function serveProxyRequest(
2222
req: IncomingMessage,
2323
res: ServerResponse,
2424
): Promise<void> {
25+
// Fetch refuses TRACE. node:http rejects TRACK and routes CONNECT to the 'connect' event.
26+
if (req.method === 'TRACE') {
27+
endWithStatus(res, 404);
28+
return;
29+
}
2530
const url = requestUrl(req);
2631
if (!url) {
2732
endWithStatus(res, 400);
2833
return;
2934
}
30-
let request: Request;
31-
try {
32-
request = toWebRequest(url, req, res);
33-
} catch {
34-
// Fetch refuses CONNECT, TRACE and TRACK; the daemon serves none of them.
35-
endWithStatus(res, 404);
36-
return;
37-
}
38-
const response = await proxy.handle(request);
35+
const response = await proxy.handle(toWebRequest(url, req, res));
3936
res.statusCode = response.status;
4037
for (const [name, value] of response.headers) res.setHeader(name, value);
4138
if (!response.body) {
@@ -50,10 +47,12 @@ function endWithStatus(res: ServerResponse, status: number): void {
5047
res.end();
5148
}
5249

53-
/** `null` when the Host header and path do not form a URL. */
50+
/** `null` when the Host header and path do not form a URL that Fetch accepts. */
5451
function requestUrl(req: IncomingMessage): URL | null {
5552
const scheme = req.socket instanceof TLSSocket ? 'https' : 'http';
56-
return URL.parse(req.url ?? '/', `${scheme}://${req.headers.host ?? '127.0.0.1'}`);
53+
const url = URL.parse(req.url ?? '/', `${scheme}://${req.headers.host ?? '127.0.0.1'}`);
54+
if (!url || url.username || url.password) return null;
55+
return url;
5756
}
5857

5958
function toWebRequest(url: URL, req: IncomingMessage, res: ServerResponse): Request {

0 commit comments

Comments
 (0)