Skip to content

Commit a6715c6

Browse files
authored
Merge branch 'webpack:master' into publicPath-output
2 parents ba4acf1 + ffd0b86 commit a6715c6

8 files changed

+391
-11
lines changed

.github/workflows/dependency-review.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- name: "Checkout Repository"
12-
uses: actions/checkout@v3
12+
uses: actions/checkout@v4
1313
- name: "Dependency Review"
14-
uses: actions/dependency-review-action@v3
14+
uses: actions/dependency-review-action@v4

lib/Server.js

+41-9
Original file line numberDiff line numberDiff line change
@@ -437,8 +437,9 @@ class Server {
437437
return network.address;
438438
});
439439

440-
for (const network of networks) {
441-
host = network.address;
440+
if (networks.length > 0) {
441+
// Take the first network found
442+
host = networks[0].address;
442443

443444
if (host.includes(":")) {
444445
host = `[${host}]`;
@@ -1960,7 +1961,7 @@ class Server {
19601961
(req.headers);
19611962
const headerName = headers[":authority"] ? ":authority" : "host";
19621963

1963-
if (this.checkHeader(headers, headerName)) {
1964+
if (this.checkHeader(headers, headerName, true)) {
19641965
next();
19651966
return;
19661967
}
@@ -1970,6 +1971,32 @@ class Server {
19701971
},
19711972
});
19721973

1974+
// Register setup cross origin request check for security
1975+
middlewares.push({
1976+
name: "cross-origin-header-check",
1977+
/**
1978+
* @param {Request} req
1979+
* @param {Response} res
1980+
* @param {NextFunction} next
1981+
* @returns {void}
1982+
*/
1983+
middleware: (req, res, next) => {
1984+
const headers =
1985+
/** @type {{ [key: string]: string | undefined }} */
1986+
(req.headers);
1987+
if (
1988+
headers["sec-fetch-mode"] === "no-cors" &&
1989+
headers["sec-fetch-site"] === "cross-site"
1990+
) {
1991+
res.statusCode = 403;
1992+
res.end("Cross-Origin request blocked");
1993+
return;
1994+
}
1995+
1996+
next();
1997+
},
1998+
});
1999+
19732000
const isHTTP2 =
19742001
/** @type {ServerConfiguration<A, S>} */ (this.options.server).type ===
19752002
"http2";
@@ -2641,8 +2668,8 @@ class Server {
26412668

26422669
if (
26432670
!headers ||
2644-
!this.checkHeader(headers, "host") ||
2645-
!this.checkHeader(headers, "origin")
2671+
!this.checkHeader(headers, "host", true) ||
2672+
!this.checkHeader(headers, "origin", false)
26462673
) {
26472674
this.sendMessage([client], "error", "Invalid Host/Origin header");
26482675

@@ -3095,9 +3122,10 @@ class Server {
30953122
* @private
30963123
* @param {{ [key: string]: string | undefined }} headers
30973124
* @param {string} headerToCheck
3125+
* @param {boolean} allowIP
30983126
* @returns {boolean}
30993127
*/
3100-
checkHeader(headers, headerToCheck) {
3128+
checkHeader(headers, headerToCheck, allowIP) {
31013129
// allow user to opt out of this security check, at their own risk
31023130
// by explicitly enabling allowedHosts
31033131
if (this.options.allowedHosts === "all") {
@@ -3124,7 +3152,10 @@ class Server {
31243152
true,
31253153
).hostname;
31263154

3127-
// always allow requests with explicit IPv4 or IPv6-address.
3155+
// allow requests with explicit IPv4 or IPv6-address if allowIP is true.
3156+
// Note that IP should not be automatically allowed for Origin headers,
3157+
// otherwise an untrusted remote IP host can send requests.
3158+
//
31283159
// A note on IPv6 addresses:
31293160
// hostHeader will always contain the brackets denoting
31303161
// an IPv6-address in URLs,
@@ -3134,8 +3165,9 @@ class Server {
31343165
// and its subdomains (hostname.endsWith(".localhost")).
31353166
// allow hostname of listening address (hostname === this.options.host)
31363167
const isValidHostname =
3137-
(hostname !== null && ipaddr.IPv4.isValid(hostname)) ||
3138-
(hostname !== null && ipaddr.IPv6.isValid(hostname)) ||
3168+
(allowIP &&
3169+
hostname !== null &&
3170+
(ipaddr.IPv4.isValid(hostname) || ipaddr.IPv6.isValid(hostname))) ||
31393171
hostname === "localhost" ||
31403172
(hostname !== null && hostname.endsWith(".localhost")) ||
31413173
hostname === this.options.host;

test/cli/host-option.test.js

+122
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use strict";
22

3+
const os = require("os");
34
const { testBin, normalizeStderr } = require("../helpers/test-bin");
45
const port = require("../ports-map")["cli-host"];
56
const Server = require("../../lib/Server");
@@ -116,6 +117,127 @@ describe('"host" CLI option', () => {
116117
expect(normalizeStderr(stderr)).toMatchSnapshot("stderr");
117118
});
118119

120+
it('should work using "--host local-ip" take the first network found', async () => {
121+
const { exitCode, stderr } = await testBin([
122+
"--port",
123+
port,
124+
"--host",
125+
"local-ip",
126+
]);
127+
128+
expect(exitCode).toEqual(0);
129+
jest.spyOn(os, "networkInterfaces").mockImplementation(() => {
130+
return {
131+
lo: [
132+
{
133+
address: "127.0.0.1",
134+
netmask: "255.0.0.0",
135+
family: "IPv4",
136+
mac: "00:00:00:00:00:00",
137+
internal: true,
138+
cidr: "127.0.0.1/8",
139+
},
140+
{
141+
address: "::1",
142+
netmask: "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
143+
family: "IPv6",
144+
mac: "00:00:00:00:00:00",
145+
internal: true,
146+
cidr: "::1/128",
147+
scopeid: 0,
148+
},
149+
],
150+
enp6s0: [
151+
{
152+
address: "192.168.1.15",
153+
netmask: "255.255.255.0",
154+
family: "IPv4",
155+
mac: "50:eb:f6:97:9f:6f",
156+
internal: false,
157+
cidr: "192.168.1.15/24",
158+
},
159+
{
160+
address: "2a01:cb0c:1623:6800:4ff8:723c:1a4b:fe5d",
161+
netmask: "ffff:ffff:ffff:ffff::",
162+
family: "IPv6",
163+
mac: "50:eb:f6:97:9f:6f",
164+
internal: false,
165+
cidr: "2a01:cb0c:1623:6800:4ff8:723c:1a4b:fe5d/64",
166+
scopeid: 0,
167+
},
168+
{
169+
address: "2a01:cb0c:1623:6800:9acc:408c:ee87:27cf",
170+
netmask: "ffff:ffff:ffff:ffff::",
171+
family: "IPv6",
172+
mac: "50:eb:f6:97:9f:6f",
173+
internal: false,
174+
cidr: "2a01:cb0c:1623:6800:9acc:408c:ee87:27cf/64",
175+
scopeid: 0,
176+
},
177+
{
178+
address: "fe80::bf2a:e5e2:8f9:4336",
179+
netmask: "ffff:ffff:ffff:ffff::",
180+
family: "IPv6",
181+
mac: "50:eb:f6:97:9f:6f",
182+
internal: false,
183+
cidr: "fe80::bf2a:e5e2:8f9:4336/64",
184+
scopeid: 2,
185+
},
186+
],
187+
"br-9bb0264f9b1c": [
188+
{
189+
address: "172.19.0.1",
190+
netmask: "255.255.0.0",
191+
family: "IPv4",
192+
mac: "02:42:e4:c8:6e:5f",
193+
internal: false,
194+
cidr: "172.19.0.1/16",
195+
},
196+
{
197+
address: "fe80::42:e4ff:fec8:6e5f",
198+
netmask: "ffff:ffff:ffff:ffff::",
199+
family: "IPv6",
200+
mac: "02:42:e4:c8:6e:5f",
201+
internal: false,
202+
cidr: "fe80::42:e4ff:fec8:6e5f/64",
203+
scopeid: 4,
204+
},
205+
],
206+
"br-a52e5d90701f": [
207+
{
208+
address: "172.18.0.1",
209+
netmask: "255.255.0.0",
210+
family: "IPv4",
211+
mac: "02:42:f6:7e:a2:45",
212+
internal: false,
213+
cidr: "172.18.0.1/16",
214+
},
215+
{
216+
address: "fe80::42:f6ff:fe7e:a245",
217+
netmask: "ffff:ffff:ffff:ffff::",
218+
family: "IPv6",
219+
mac: "02:42:f6:7e:a2:45",
220+
internal: false,
221+
cidr: "fe80::42:f6ff:fe7e:a245/64",
222+
scopeid: 5,
223+
},
224+
],
225+
docker0: [
226+
{
227+
address: "172.17.0.1",
228+
netmask: "255.255.0.0",
229+
family: "IPv4",
230+
mac: "02:42:3e:89:61:cf",
231+
internal: false,
232+
cidr: "172.17.0.1/16",
233+
},
234+
],
235+
};
236+
});
237+
expect(stderr.indexOf("172.17.0.1") === -1);
238+
expect(stderr.indexOf("192.168.1.15") > -1);
239+
});
240+
119241
it('should work using "--host local-ipv4"', async () => {
120242
const { exitCode, stderr } = await testBin([
121243
"--port",

test/e2e/__snapshots__/allowed-hosts.test.js.snap.webpack5

+26
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,32 @@ exports[`allowed hosts should disconnect web client using localhost to web socke
282282

283283
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("ws"): page errors 1`] = `[]`;
284284

285+
exports[`allowed hosts should disconnect web client with origin header containing an IP address with the "auto" value ("sockjs"): console messages 1`] = `
286+
[
287+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
288+
"[HMR] Waiting for update signal from WDS...",
289+
"Hey.",
290+
"[webpack-dev-server] Invalid Host/Origin header",
291+
"[webpack-dev-server] Disconnected!",
292+
"[webpack-dev-server] Trying to reconnect...",
293+
]
294+
`;
295+
296+
exports[`allowed hosts should disconnect web client with origin header containing an IP address with the "auto" value ("sockjs"): page errors 1`] = `[]`;
297+
298+
exports[`allowed hosts should disconnect web client with origin header containing an IP address with the "auto" value ("ws"): console messages 1`] = `
299+
[
300+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
301+
"[HMR] Waiting for update signal from WDS...",
302+
"Hey.",
303+
"[webpack-dev-server] Invalid Host/Origin header",
304+
"[webpack-dev-server] Disconnected!",
305+
"[webpack-dev-server] Trying to reconnect...",
306+
]
307+
`;
308+
309+
exports[`allowed hosts should disconnect web client with origin header containing an IP address with the "auto" value ("ws"): page errors 1`] = `[]`;
310+
285311
exports[`allowed hosts should disconnect web socket client using custom hostname from web socket server with the "auto" value based on the "host" header ("sockjs"): console messages 1`] = `
286312
[
287313
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",

test/e2e/allowed-hosts.test.js

+80
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,86 @@ describe("allowed hosts", () => {
12061206
await server.stop();
12071207
}
12081208
});
1209+
1210+
it(`should disconnect web client with origin header containing an IP address with the "auto" value ("${webSocketServer}")`, async () => {
1211+
const devServerHost = "127.0.0.1";
1212+
const devServerPort = port1;
1213+
const proxyHost = devServerHost;
1214+
const proxyPort = port2;
1215+
1216+
const compiler = webpack(config);
1217+
const devServerOptions = {
1218+
client: {
1219+
webSocketURL: {
1220+
port: port2,
1221+
},
1222+
},
1223+
webSocketServer,
1224+
port: devServerPort,
1225+
host: devServerHost,
1226+
allowedHosts: "auto",
1227+
};
1228+
const server = new Server(devServerOptions, compiler);
1229+
1230+
await server.start();
1231+
1232+
function startProxy(callback) {
1233+
const app = express();
1234+
1235+
app.use(
1236+
"/",
1237+
createProxyMiddleware({
1238+
// Emulation
1239+
onProxyReqWs: (proxyReq) => {
1240+
proxyReq.setHeader("origin", "http://192.168.1.1/");
1241+
},
1242+
target: `http://${devServerHost}:${devServerPort}`,
1243+
ws: true,
1244+
changeOrigin: true,
1245+
logLevel: "warn",
1246+
}),
1247+
);
1248+
1249+
return app.listen(proxyPort, proxyHost, callback);
1250+
}
1251+
1252+
const proxy = await new Promise((resolve) => {
1253+
const proxyCreated = startProxy(() => {
1254+
resolve(proxyCreated);
1255+
});
1256+
});
1257+
1258+
const { page, browser } = await runBrowser();
1259+
1260+
try {
1261+
const pageErrors = [];
1262+
const consoleMessages = [];
1263+
1264+
page
1265+
.on("console", (message) => {
1266+
consoleMessages.push(message);
1267+
})
1268+
.on("pageerror", (error) => {
1269+
pageErrors.push(error);
1270+
});
1271+
1272+
await page.goto(`http://${proxyHost}:${proxyPort}/`, {
1273+
waitUntil: "networkidle0",
1274+
});
1275+
1276+
expect(
1277+
consoleMessages.map((message) => message.text()),
1278+
).toMatchSnapshot("console messages");
1279+
expect(pageErrors).toMatchSnapshot("page errors");
1280+
} catch (error) {
1281+
throw error;
1282+
} finally {
1283+
proxy.close();
1284+
1285+
await browser.close();
1286+
await server.stop();
1287+
}
1288+
});
12091289
}
12101290

12111291
describe("check host headers", () => {

0 commit comments

Comments
 (0)