Skip to content

Commit f21ed0f

Browse files
authored
fix: handle malformed Host and Origin headers (#5699)
1 parent 80cd9ee commit f21ed0f

3 files changed

Lines changed: 120 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack-dev-server": patch
3+
---
4+
5+
Handle malformed `Host` and `Origin` header values gracefully when validating requests.

lib/Server.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3304,14 +3304,20 @@ class Server {
33043304
}
33053305

33063306
// use the node url-parser to retrieve the hostname from the host-header.
3307-
// TODO resolve me in the next major release
3308-
// eslint-disable-next-line n/no-deprecated-api
3309-
const { hostname } = url.parse(
3307+
let hostname;
3308+
3309+
try {
33103310
// if header doesn't have scheme, add // for parsing.
3311-
/^(.+:)?\/\//.test(header) ? header : `//${header}`,
3312-
false,
3313-
true,
3314-
);
3311+
// TODO resolve me in the next major release
3312+
// eslint-disable-next-line n/no-deprecated-api
3313+
({ hostname } = url.parse(
3314+
/^(.+:)?\/\//.test(header) ? header : `//${header}`,
3315+
false,
3316+
true,
3317+
));
3318+
} catch {
3319+
return false;
3320+
}
33153321

33163322
if (hostname === null) {
33173323
return false;

test/e2e/cross-origin-request.test.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,3 +463,105 @@ describe("cross-site request forgery on state-changing endpoints", () => {
463463
expect(res.status).toBe(200);
464464
});
465465
});
466+
467+
describe("malformed Host/Origin headers", () => {
468+
const devServerPort = port1;
469+
470+
let server;
471+
472+
afterEach(async () => {
473+
if (server) {
474+
await server.stop();
475+
// Allow the port to be fully released before the next test
476+
await new Promise((resolve) => {
477+
setTimeout(resolve, 100);
478+
});
479+
server = null;
480+
}
481+
});
482+
483+
it("should reject a WebSocket upgrade with a malformed Origin header without crashing", async () => {
484+
const WebSocket = require("ws");
485+
const http = require("node:http");
486+
487+
const compiler = webpack(config);
488+
server = new Server(
489+
{ port: devServerPort, allowedHosts: "auto" },
490+
compiler,
491+
);
492+
493+
await server.start();
494+
495+
// A malformed `Origin` (invalid IPv6 literal) used to throw while being
496+
// parsed and take down the whole dev-server process.
497+
await new Promise((resolve) => {
498+
const ws = new WebSocket(`ws://localhost:${devServerPort}/ws`, {
499+
headers: {
500+
host: `localhost:${devServerPort}`,
501+
origin: "http://[::1/",
502+
},
503+
});
504+
505+
ws.on("close", resolve);
506+
ws.on("error", resolve);
507+
});
508+
509+
// The server must still be alive: a normal request still succeeds.
510+
const status = await new Promise((resolve, reject) => {
511+
http
512+
.get(`http://localhost:${devServerPort}/main.js`, (res) => {
513+
res.resume();
514+
resolve(res.statusCode);
515+
})
516+
.on("error", reject);
517+
});
518+
519+
expect(status).toBe(200);
520+
});
521+
522+
it("should reject a request with a malformed Host header without crashing", async () => {
523+
const net = require("node:net");
524+
const http = require("node:http");
525+
526+
const compiler = webpack(config);
527+
server = new Server(
528+
{ port: devServerPort, allowedHosts: "auto" },
529+
compiler,
530+
);
531+
532+
await server.start();
533+
534+
// A malformed `Host` (invalid IPv6 literal) sent on a plain request used to
535+
// throw while being parsed and take down the whole dev-server process. Sent
536+
// over a raw socket so the malformed value reaches the server as-is.
537+
await new Promise((resolve) => {
538+
const socket = net.connect(devServerPort, "127.0.0.1", () => {
539+
socket.write(
540+
[
541+
"GET /main.js HTTP/1.1",
542+
"Host: [::1",
543+
"Connection: close",
544+
"",
545+
"",
546+
].join("\r\n"),
547+
);
548+
});
549+
550+
socket.on("data", () => {});
551+
socket.on("close", resolve);
552+
socket.on("error", resolve);
553+
});
554+
555+
// The server must still be alive: a normal request still succeeds.
556+
const status = await new Promise((resolve, reject) => {
557+
http
558+
.get(`http://localhost:${devServerPort}/main.js`, (res) => {
559+
res.resume();
560+
resolve(res.statusCode);
561+
})
562+
.on("error", reject);
563+
});
564+
565+
expect(status).toBe(200);
566+
});
567+
});

0 commit comments

Comments
 (0)