Files
bun.sh/test/regression/issue/24388.test.ts
robobun 362839c987 fix(websocket): forward URL credentials as Authorization header (#26278)
## Summary

- Extracts credentials from WebSocket URL (`ws://user:pass@host`) and
sends them as Basic Authorization header
- User-provided `Authorization` header takes precedence over URL
credentials
- Credentials are properly URL-decoded before being Base64-encoded

Fixes #24388

## Test plan

- [x] Added regression test `test/regression/issue/24388.test.ts` with 5
test cases:
  - Basic credentials in URL
  - Empty password
  - No credentials (no header sent)
  - Custom Authorization header takes precedence
  - Special characters (URL-encoded) in credentials
- [x] Tests pass with `bun bd test test/regression/issue/24388.test.ts`
- [x] Tests fail with `USE_SYSTEM_BUN=1 bun test` (confirming the bug
existed)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 17:04:44 -08:00

203 lines
6.6 KiB
TypeScript

import { expect, test } from "bun:test";
// Test for GitHub issue #24388
// WebSocket should forward Basic Authentication credentials from URL to server
test("WebSocket URL with embedded credentials sends Authorization header", async () => {
using server = Bun.serve({
port: 0,
fetch(req, server) {
if (req.headers.get("upgrade") === "websocket") {
const authHeader = req.headers.get("authorization");
if (server.upgrade(req, { data: { authHeader } })) {
return undefined;
}
return new Response("Upgrade failed", { status: 500 });
}
return new Response("Not Found", { status: 404 });
},
websocket: {
open(ws) {
ws.send((ws.data as { authHeader: string | null }).authHeader ?? "null");
},
message() {},
close() {},
},
});
const { promise: messagePromise, resolve: resolveMessage, reject } = Promise.withResolvers<string>();
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<void>();
const ws = new WebSocket(`ws://testuser:testpass@localhost:${server.port}/`);
ws.onmessage = event => {
resolveMessage(event.data);
ws.close();
};
ws.onerror = () => reject(new Error("WebSocket error"));
ws.onclose = () => resolveClose();
const authHeader = await messagePromise;
const expected = `Basic ${Buffer.from("testuser:testpass").toString("base64")}`;
expect(authHeader).toBe(expected);
await closePromise;
});
test("WebSocket URL with empty password sends Authorization header", async () => {
using server = Bun.serve({
port: 0,
fetch(req, server) {
if (req.headers.get("upgrade") === "websocket") {
const authHeader = req.headers.get("authorization");
if (server.upgrade(req, { data: { authHeader } })) {
return undefined;
}
return new Response("Upgrade failed", { status: 500 });
}
return new Response("Not Found", { status: 404 });
},
websocket: {
open(ws) {
ws.send((ws.data as { authHeader: string | null }).authHeader ?? "null");
},
message() {},
close() {},
},
});
const { promise: messagePromise, resolve: resolveMessage, reject } = Promise.withResolvers<string>();
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<void>();
const ws = new WebSocket(`ws://testuser:@localhost:${server.port}/`);
ws.onmessage = event => {
resolveMessage(event.data);
ws.close();
};
ws.onerror = () => reject(new Error("WebSocket error"));
ws.onclose = () => resolveClose();
const authHeader = await messagePromise;
const expected = `Basic ${Buffer.from("testuser:").toString("base64")}`;
expect(authHeader).toBe(expected);
await closePromise;
});
test("WebSocket URL without credentials does not send Authorization header", async () => {
using server = Bun.serve({
port: 0,
fetch(req, server) {
if (req.headers.get("upgrade") === "websocket") {
const authHeader = req.headers.get("authorization");
if (server.upgrade(req, { data: { authHeader } })) {
return undefined;
}
return new Response("Upgrade failed", { status: 500 });
}
return new Response("Not Found", { status: 404 });
},
websocket: {
open(ws) {
ws.send((ws.data as { authHeader: string | null }).authHeader ?? "null");
},
message() {},
close() {},
},
});
const { promise: messagePromise, resolve: resolveMessage, reject } = Promise.withResolvers<string>();
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<void>();
const ws = new WebSocket(`ws://localhost:${server.port}/`);
ws.onmessage = event => {
resolveMessage(event.data);
ws.close();
};
ws.onerror = () => reject(new Error("WebSocket error"));
ws.onclose = () => resolveClose();
const authHeader = await messagePromise;
expect(authHeader).toBe("null");
await closePromise;
});
test("WebSocket custom Authorization header takes precedence over URL credentials", async () => {
using server = Bun.serve({
port: 0,
fetch(req, server) {
if (req.headers.get("upgrade") === "websocket") {
const authHeader = req.headers.get("authorization");
if (server.upgrade(req, { data: { authHeader } })) {
return undefined;
}
return new Response("Upgrade failed", { status: 500 });
}
return new Response("Not Found", { status: 404 });
},
websocket: {
open(ws) {
ws.send((ws.data as { authHeader: string | null }).authHeader ?? "null");
},
message() {},
close() {},
},
});
const { promise: messagePromise, resolve: resolveMessage, reject } = Promise.withResolvers<string>();
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<void>();
const ws = new WebSocket(`ws://testuser:testpass@localhost:${server.port}/`, {
headers: {
Authorization: "Bearer custom-token",
},
});
ws.onmessage = event => {
resolveMessage(event.data);
ws.close();
};
ws.onerror = () => reject(new Error("WebSocket error"));
ws.onclose = () => resolveClose();
const authHeader = await messagePromise;
expect(authHeader).toBe("Bearer custom-token");
await closePromise;
});
test("WebSocket URL with special characters in credentials sends Authorization header", async () => {
using server = Bun.serve({
port: 0,
fetch(req, server) {
if (req.headers.get("upgrade") === "websocket") {
const authHeader = req.headers.get("authorization");
if (server.upgrade(req, { data: { authHeader } })) {
return undefined;
}
return new Response("Upgrade failed", { status: 500 });
}
return new Response("Not Found", { status: 404 });
},
websocket: {
open(ws) {
ws.send((ws.data as { authHeader: string | null }).authHeader ?? "null");
},
message() {},
close() {},
},
});
const { promise: messagePromise, resolve: resolveMessage, reject } = Promise.withResolvers<string>();
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<void>();
// URL-encoded special characters (user@example.com:p@ss:word)
const ws = new WebSocket(`ws://user%40example.com:p%40ss%3Aword@localhost:${server.port}/`);
ws.onmessage = event => {
resolveMessage(event.data);
ws.close();
};
ws.onerror = () => reject(new Error("WebSocket error"));
ws.onclose = () => resolveClose();
const authHeader = await messagePromise;
const expected = `Basic ${Buffer.from("user@example.com:p@ss:word").toString("base64")}`;
expect(authHeader).toBe(expected);
await closePromise;
});