Skip to content

Commit cb90876

Browse files
refactor: logic
1 parent 3d1730e commit cb90876

File tree

4 files changed

+666
-97
lines changed

4 files changed

+666
-97
lines changed

lib/Server.js

Lines changed: 129 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,8 @@ function useFn(route, fn) {
307307
return /** @type {BasicApplication} */ ({});
308308
}
309309

310+
const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i;
311+
310312
/**
311313
* @typedef {Object} BasicApplication
312314
* @property {typeof useFn} use
@@ -1961,7 +1963,7 @@ class Server {
19611963
(req.headers);
19621964
const headerName = headers[":authority"] ? ":authority" : "host";
19631965

1964-
if (this.checkHeader(headers, headerName)) {
1966+
if (this.isValidHost(headers, headerName)) {
19651967
next();
19661968
return;
19671969
}
@@ -2668,8 +2670,9 @@ class Server {
26682670

26692671
if (
26702672
!headers ||
2671-
!this.checkHeader(headers, "host") ||
2672-
!this.checkHeader(headers, "origin")
2673+
!this.isValidHost(headers, "host") ||
2674+
!this.isValidHost(headers, "origin") ||
2675+
!this.isSameOrigin(headers)
26732676
) {
26742677
this.sendMessage([client], "error", "Invalid Host/Origin header");
26752678

@@ -3107,101 +3110,174 @@ class Server {
31073110

31083111
/**
31093112
* @private
3110-
* @param {{ [key: string]: string | undefined }} headers
3111-
* @param {string} headerToCheck
3113+
* @param {string} value
31123114
* @returns {boolean}
31133115
*/
3114-
checkHeader(headers, headerToCheck) {
3116+
isHostAllowed(value) {
3117+
const { allowedHosts } = this.options;
3118+
31153119
// allow user to opt out of this security check, at their own risk
31163120
// by explicitly enabling allowedHosts
3117-
if (this.options.allowedHosts === "all") {
3121+
if (allowedHosts === "all") {
31183122
return true;
31193123
}
31203124

3125+
// always allow localhost host, for convenience
3126+
// allow if value is in allowedHosts
3127+
if (Array.isArray(allowedHosts) && allowedHosts.length > 0) {
3128+
for (let hostIdx = 0; hostIdx < allowedHosts.length; hostIdx++) {
3129+
/** @type {string} */
3130+
const allowedHost = allowedHosts[hostIdx];
3131+
3132+
if (allowedHost === value) {
3133+
return true;
3134+
}
3135+
3136+
// support "." as a subdomain wildcard
3137+
// e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
3138+
if (allowedHost[0] === ".") {
3139+
// "example.com" (value === allowedHost.substring(1))
3140+
// "*.example.com" (value.endsWith(allowedHost))
3141+
if (
3142+
value === allowedHost.substring(1) ||
3143+
/** @type {string} */
3144+
(value).endsWith(allowedHost)
3145+
) {
3146+
return true;
3147+
}
3148+
}
3149+
}
3150+
}
3151+
3152+
// Also allow if `client.webSocketURL.hostname` provided
3153+
if (
3154+
this.options.client &&
3155+
typeof (
3156+
/** @type {ClientConfiguration} */
3157+
(this.options.client).webSocketURL
3158+
) !== "undefined"
3159+
) {
3160+
return (
3161+
/** @type {WebSocketURL} */
3162+
(/** @type {ClientConfiguration} */ (this.options.client).webSocketURL)
3163+
.hostname === value
3164+
);
3165+
}
3166+
3167+
return false;
3168+
}
3169+
3170+
/**
3171+
* @private
3172+
* @param {{ [key: string]: string | undefined }} headers
3173+
* @param {string} headerToCheck
3174+
* @returns {boolean}
3175+
*/
3176+
isValidHost(headers, headerToCheck) {
31213177
// get the Host header and extract hostname
31223178
// we don't care about port not matching
3123-
const hostHeader = headers[headerToCheck];
3179+
const header = headers[headerToCheck];
31243180

3125-
if (!hostHeader) {
3181+
if (!header) {
31263182
return false;
31273183
}
31283184

3129-
if (/^(file|.+-extension):/i.test(hostHeader)) {
3185+
if (DEFAULT_ALLOWED_PROTOCOLS.test(header)) {
31303186
return true;
31313187
}
31323188

31333189
// use the node url-parser to retrieve the hostname from the host-header.
31343190
const hostname = url.parse(
3135-
// if hostHeader doesn't have scheme, add // for parsing.
3136-
/^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
3191+
// if header doesn't have scheme, add // for parsing.
3192+
/^(.+:)?\/\//.test(header) ? header : `//${header}`,
31373193
false,
31383194
true,
31393195
).hostname;
31403196

3197+
if (hostname === null) {
3198+
return false;
3199+
}
3200+
3201+
if (this.isHostAllowed(hostname)) {
3202+
return true;
3203+
}
3204+
31413205
// always allow requests with explicit IPv4 or IPv6-address.
31423206
// A note on IPv6 addresses:
3143-
// hostHeader will always contain the brackets denoting
3207+
// header will always contain the brackets denoting
31443208
// an IPv6-address in URLs,
31453209
// these are removed from the hostname in url.parse(),
31463210
// so we have the pure IPv6-address in hostname.
31473211
// For convenience, always allow localhost (hostname === 'localhost')
31483212
// and its subdomains (hostname.endsWith(".localhost")).
31493213
// allow hostname of listening address (hostname === this.options.host)
31503214
const isValidHostname =
3151-
(hostname !== null &&
3152-
(ipaddr.IPv4.isValid(hostname) || ipaddr.IPv6.isValid(hostname))) ||
3215+
ipaddr.IPv4.isValid(hostname) ||
3216+
ipaddr.IPv6.isValid(hostname) ||
31533217
hostname === "localhost" ||
3154-
(hostname !== null && hostname.endsWith(".localhost")) ||
3218+
hostname.endsWith(".localhost") ||
31553219
hostname === this.options.host;
31563220

31573221
if (isValidHostname) {
31583222
return true;
31593223
}
31603224

3161-
const { allowedHosts } = this.options;
3225+
// disallow
3226+
return false;
3227+
}
31623228

3163-
// always allow localhost host, for convenience
3164-
// allow if hostname is in allowedHosts
3165-
if (Array.isArray(allowedHosts) && allowedHosts.length > 0) {
3166-
for (let hostIdx = 0; hostIdx < allowedHosts.length; hostIdx++) {
3167-
/** @type {string} */
3168-
const allowedHost = allowedHosts[hostIdx];
3229+
/**
3230+
* @private
3231+
* @param {{ [key: string]: string | undefined }} headers
3232+
* @returns {boolean}
3233+
*/
3234+
isSameOrigin(headers) {
3235+
const originHeader = headers.origin;
31693236

3170-
if (allowedHost === hostname) {
3171-
return true;
3172-
}
3237+
if (!originHeader) {
3238+
return true;
3239+
}
31733240

3174-
// support "." as a subdomain wildcard
3175-
// e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
3176-
if (allowedHost[0] === ".") {
3177-
// "example.com" (hostname === allowedHost.substring(1))
3178-
// "*.example.com" (hostname.endsWith(allowedHost))
3179-
if (
3180-
hostname === allowedHost.substring(1) ||
3181-
/** @type {string} */ (hostname).endsWith(allowedHost)
3182-
) {
3183-
return true;
3184-
}
3185-
}
3186-
}
3241+
if (DEFAULT_ALLOWED_PROTOCOLS.test(originHeader)) {
3242+
return true;
31873243
}
31883244

3189-
// Also allow if `client.webSocketURL.hostname` provided
3190-
if (
3191-
this.options.client &&
3192-
typeof (
3193-
/** @type {ClientConfiguration} */ (this.options.client).webSocketURL
3194-
) !== "undefined"
3195-
) {
3196-
return (
3197-
/** @type {WebSocketURL} */
3198-
(/** @type {ClientConfiguration} */ (this.options.client).webSocketURL)
3199-
.hostname === hostname
3200-
);
3245+
const origin = url.parse(originHeader, false, true).hostname;
3246+
3247+
if (origin === null) {
3248+
return false;
32013249
}
32023250

3203-
// disallow
3204-
return false;
3251+
if (this.isHostAllowed(origin)) {
3252+
return true;
3253+
}
3254+
3255+
const hostHeader = headers.host;
3256+
3257+
if (!hostHeader) {
3258+
return false;
3259+
}
3260+
3261+
if (DEFAULT_ALLOWED_PROTOCOLS.test(hostHeader)) {
3262+
return true;
3263+
}
3264+
3265+
const host = url.parse(
3266+
// if hostHeader doesn't have scheme, add // for parsing.
3267+
/^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
3268+
false,
3269+
true,
3270+
).hostname;
3271+
3272+
if (host === null) {
3273+
return false;
3274+
}
3275+
3276+
if (this.isHostAllowed(host)) {
3277+
return true;
3278+
}
3279+
3280+
return origin === host;
32053281
}
32063282

32073283
/**

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

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,46 @@ exports[`allowed hosts should connect web socket client using "[::1] host to web
6262

6363
exports[`allowed hosts should connect web socket client using "[::1] host to web socket server with the "auto" value ("ws"): page errors 1`] = `[]`;
6464

65+
exports[`allowed hosts should connect web socket client using "0.0.0.0" host to web socket server with the "auto" value ("sockjs"): console messages 1`] = `
66+
[
67+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
68+
"[HMR] Waiting for update signal from WDS...",
69+
"Hey.",
70+
]
71+
`;
72+
73+
exports[`allowed hosts should connect web socket client using "0.0.0.0" host to web socket server with the "auto" value ("sockjs"): page errors 1`] = `[]`;
74+
75+
exports[`allowed hosts should connect web socket client using "0.0.0.0" host to web socket server with the "auto" value ("ws"): console messages 1`] = `
76+
[
77+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
78+
"[HMR] Waiting for update signal from WDS...",
79+
"Hey.",
80+
]
81+
`;
82+
83+
exports[`allowed hosts should connect web socket client using "0.0.0.0" host to web socket server with the "auto" value ("ws"): page errors 1`] = `[]`;
84+
85+
exports[`allowed hosts should connect web socket client using "127.0.0.1" host to web socket server by default ("sockjs"): console messages 1`] = `
86+
[
87+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
88+
"[HMR] Waiting for update signal from WDS...",
89+
"Hey.",
90+
]
91+
`;
92+
93+
exports[`allowed hosts should connect web socket client using "127.0.0.1" host to web socket server by default ("sockjs"): page errors 1`] = `[]`;
94+
95+
exports[`allowed hosts should connect web socket client using "127.0.0.1" host to web socket server by default ("ws"): console messages 1`] = `
96+
[
97+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
98+
"[HMR] Waiting for update signal from WDS...",
99+
"Hey.",
100+
]
101+
`;
102+
103+
exports[`allowed hosts should connect web socket client using "127.0.0.1" host to web socket server by default ("ws"): page errors 1`] = `[]`;
104+
65105
exports[`allowed hosts should connect web socket client using "127.0.0.1" host to web socket server with the "auto" value ("sockjs"): console messages 1`] = `
66106
[
67107
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
@@ -122,6 +162,26 @@ exports[`allowed hosts should connect web socket client using "file:" protocol t
122162

123163
exports[`allowed hosts should connect web socket client using "file:" protocol to web socket server with the "auto" value ("ws"): page errors 1`] = `[]`;
124164

165+
exports[`allowed hosts should connect web socket client using "localhost" host to web socket server by default ("sockjs"): console messages 1`] = `
166+
[
167+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
168+
"[HMR] Waiting for update signal from WDS...",
169+
"Hey.",
170+
]
171+
`;
172+
173+
exports[`allowed hosts should connect web socket client using "localhost" host to web socket server by default ("sockjs"): page errors 1`] = `[]`;
174+
175+
exports[`allowed hosts should connect web socket client using "localhost" host to web socket server by default ("ws"): console messages 1`] = `
176+
[
177+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
178+
"[HMR] Waiting for update signal from WDS...",
179+
"Hey.",
180+
]
181+
`;
182+
183+
exports[`allowed hosts should connect web socket client using "localhost" host to web socket server by default ("ws"): page errors 1`] = `[]`;
184+
125185
exports[`allowed hosts should connect web socket client using custom hostname to web socket server with the "all" value ("sockjs"): console messages 1`] = `
126186
[
127187
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
@@ -262,6 +322,26 @@ exports[`allowed hosts should connect web socket client using localhost to web s
262322

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

325+
exports[`allowed hosts should connect web socket client using origin header containing an IP address with the custom hostname value ("sockjs"): (work) console messages 1`] = `
326+
[
327+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
328+
"[HMR] Waiting for update signal from WDS...",
329+
"Hey.",
330+
]
331+
`;
332+
333+
exports[`allowed hosts should connect web socket client using origin header containing an IP address with the custom hostname value ("sockjs"): (work) page errors 1`] = `[]`;
334+
335+
exports[`allowed hosts should connect web socket client using origin header containing an IP address with the custom hostname value ("ws"): (work) console messages 1`] = `
336+
[
337+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
338+
"[HMR] Waiting for update signal from WDS...",
339+
"Hey.",
340+
]
341+
`;
342+
343+
exports[`allowed hosts should connect web socket client using origin header containing an IP address with the custom hostname value ("ws"): (work) page errors 1`] = `[]`;
344+
265345
exports[`allowed hosts should disconnect web client using localhost to web socket server with the "auto" value ("sockjs"): console messages 1`] = `
266346
[
267347
"Failed to load resource: the server responded with a status of 403 (Forbidden)",
@@ -282,6 +362,32 @@ exports[`allowed hosts should disconnect web client using localhost to web socke
282362

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

365+
exports[`allowed hosts should disconnect web client using origin header containing an IP address with the "auto" value ("sockjs"): (work) console messages 1`] = `
366+
[
367+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
368+
"[HMR] Waiting for update signal from WDS...",
369+
"Hey.",
370+
"[webpack-dev-server] Invalid Host/Origin header",
371+
"[webpack-dev-server] Disconnected!",
372+
"[webpack-dev-server] Trying to reconnect...",
373+
]
374+
`;
375+
376+
exports[`allowed hosts should disconnect web client using origin header containing an IP address with the "auto" value ("sockjs"): (work) page errors 1`] = `[]`;
377+
378+
exports[`allowed hosts should disconnect web client using origin header containing an IP address with the "auto" value ("ws"): (work) console messages 1`] = `
379+
[
380+
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",
381+
"[HMR] Waiting for update signal from WDS...",
382+
"Hey.",
383+
"[webpack-dev-server] Invalid Host/Origin header",
384+
"[webpack-dev-server] Disconnected!",
385+
"[webpack-dev-server] Trying to reconnect...",
386+
]
387+
`;
388+
389+
exports[`allowed hosts should disconnect web client using origin header containing an IP address with the "auto" value ("ws"): (work) page errors 1`] = `[]`;
390+
285391
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`] = `
286392
[
287393
"[webpack-dev-server] Server started: Hot Module Replacement enabled, Live Reloading enabled, Progress disabled, Overlay enabled.",

0 commit comments

Comments
 (0)