Skip to content

Commit 8de7782

Browse files
alexander-akaitkretajak
authored andcommitted
fix: respect the allowedHosts option for cross-origin header check (webpack#5510)
1 parent ba2e692 commit 8de7782

File tree

5 files changed

+251
-52
lines changed

5 files changed

+251
-52
lines changed

lib/Server.js

Lines changed: 122 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ const encodeOverlaySettings = (setting) =>
250250
? encodeURIComponent(setting.toString())
251251
: setting;
252252

253+
const DEFAULT_ALLOWED_PROTOCOLS = /^(file|.+-extension):/i;
254+
253255
class Server {
254256
/**
255257
* @param {Configuration | Compiler | MultiCompiler} options
@@ -2011,7 +2013,7 @@ class Server {
20112013
*/
20122014
(req, res, next) => {
20132015
if (
2014-
this.checkHeader(
2016+
this.isValidHost(
20152017
/** @type {{ [key: string]: string | undefined }} */
20162018
(req.headers),
20172019
"host",
@@ -2222,6 +2224,14 @@ class Server {
22222224
const headers =
22232225
/** @type {{ [key: string]: string | undefined }} */
22242226
(req.headers);
2227+
2228+
const headerName = headers[":authority"] ? ":authority" : "host";
2229+
2230+
if (this.isValidHost(headers, headerName, false)) {
2231+
next();
2232+
return;
2233+
}
2234+
22252235
if (
22262236
headers["sec-fetch-mode"] === "no-cors" &&
22272237
headers["sec-fetch-site"] === "cross-site"
@@ -2625,8 +2635,8 @@ class Server {
26252635

26262636
if (
26272637
!headers ||
2628-
!this.checkHeader(headers, "host", true) ||
2629-
!this.checkHeader(headers, "origin", false)
2638+
!this.isValidHost(headers, "host", true) ||
2639+
!this.isValidHost(headers, "origin", false)
26302640
) {
26312641
this.sendMessage([client], "error", "Invalid Host/Origin header");
26322642

@@ -3082,80 +3092,93 @@ class Server {
30823092
* @private
30833093
* @param {{ [key: string]: string | undefined }} headers
30843094
* @param {string} headerToCheck
3085-
* @param {boolean} allowIP
3095+
* @param {boolean} validateHost
30863096
* @returns {boolean}
30873097
*/
3088-
checkHeader(headers, headerToCheck, allowIP) {
3089-
// allow user to opt out of this security check, at their own risk
3090-
// by explicitly enabling allowedHosts
3098+
isValidHost(headers, headerToCheck, validateHost = true) {
30913099
if (this.options.allowedHosts === "all") {
30923100
return true;
30933101
}
30943102

30953103
// get the Host header and extract hostname
30963104
// we don't care about port not matching
3097-
const hostHeader = headers[headerToCheck];
3105+
const header = headers[headerToCheck];
30983106

3099-
if (!hostHeader) {
3107+
if (!header) {
31003108
return false;
31013109
}
31023110

3103-
if (/^(file|.+-extension):/i.test(hostHeader)) {
3111+
if (DEFAULT_ALLOWED_PROTOCOLS.test(header)) {
31043112
return true;
31053113
}
31063114

31073115
// use the node url-parser to retrieve the hostname from the host-header.
31083116
const hostname = url.parse(
3109-
// if hostHeader doesn't have scheme, add // for parsing.
3110-
/^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
3117+
// if header doesn't have scheme, add // for parsing.
3118+
/^(.+:)?\/\//.test(header) ? header : `//${header}`,
31113119
false,
31123120
true
31133121
).hostname;
31143122

3115-
// allow requests with explicit IPv4 or IPv6-address if allowIP is true.
3116-
// Note that IP should not be automatically allowed for Origin headers,
3117-
// otherwise an untrusted remote IP host can send requests.
3118-
//
3123+
if (hostname === null) {
3124+
return false;
3125+
}
3126+
3127+
if (this.isHostAllowed(hostname)) {
3128+
return true;
3129+
}
3130+
3131+
// always allow requests with explicit IPv4 or IPv6-address.
31193132
// A note on IPv6 addresses:
3120-
// hostHeader will always contain the brackets denoting
3133+
// header will always contain the brackets denoting
31213134
// an IPv6-address in URLs,
31223135
// these are removed from the hostname in url.parse(),
31233136
// so we have the pure IPv6-address in hostname.
31243137
// For convenience, always allow localhost (hostname === 'localhost')
31253138
// and its subdomains (hostname.endsWith(".localhost")).
31263139
// allow hostname of listening address (hostname === this.options.host)
3127-
const isValidHostname =
3128-
(allowIP &&
3129-
hostname !== null &&
3130-
(ipaddr.IPv4.isValid(hostname) || ipaddr.IPv6.isValid(hostname))) ||
3131-
hostname === "localhost" ||
3132-
(hostname !== null && hostname.endsWith(".localhost")) ||
3133-
hostname === this.options.host;
3134-
3135-
if (isValidHostname) {
3136-
return true;
3137-
}
3140+
const isValidHostname = validateHost
3141+
? ipaddr.IPv4.isValid(hostname) ||
3142+
ipaddr.IPv6.isValid(hostname) ||
3143+
hostname === "localhost" ||
3144+
hostname.endsWith(".localhost") ||
3145+
hostname === this.options.host
3146+
: true;
3147+
3148+
return isValidHostname;
3149+
}
31383150

3151+
/**
3152+
* @private
3153+
* @param {string} value
3154+
* @returns {boolean}
3155+
*/
3156+
isHostAllowed(value) {
31393157
const { allowedHosts } = this.options;
31403158

3159+
// allow user to opt out of this security check, at their own risk
3160+
// by explicitly enabling allowedHosts
3161+
if (allowedHosts === "all") {
3162+
return true;
3163+
}
3164+
31413165
// always allow localhost host, for convenience
3142-
// allow if hostname is in allowedHosts
3166+
// allow if value is in allowedHosts
31433167
if (Array.isArray(allowedHosts) && allowedHosts.length > 0) {
3144-
for (let hostIdx = 0; hostIdx < allowedHosts.length; hostIdx++) {
3145-
const allowedHost = allowedHosts[hostIdx];
3146-
3147-
if (allowedHost === hostname) {
3168+
for (const allowedHost of allowedHosts) {
3169+
if (allowedHost === value) {
31483170
return true;
31493171
}
31503172

31513173
// support "." as a subdomain wildcard
31523174
// e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
3153-
if (allowedHost[0] === ".") {
3154-
// "example.com" (hostname === allowedHost.substring(1))
3155-
// "*.example.com" (hostname.endsWith(allowedHost))
3175+
if (allowedHost.startsWith(".")) {
3176+
// "example.com" (value === allowedHost.substring(1))
3177+
// "*.example.com" (value.endsWith(allowedHost))
31563178
if (
3157-
hostname === allowedHost.substring(1) ||
3158-
/** @type {string} */ (hostname).endsWith(allowedHost)
3179+
value === allowedHost.substring(1) ||
3180+
/** @type {string} */
3181+
(value).endsWith(allowedHost)
31593182
) {
31603183
return true;
31613184
}
@@ -3167,17 +3190,17 @@ class Server {
31673190
if (
31683191
this.options.client &&
31693192
typeof (
3170-
/** @type {ClientConfiguration} */ (this.options.client).webSocketURL
3193+
/** @type {ClientConfiguration} */
3194+
(this.options.client).webSocketURL
31713195
) !== "undefined"
31723196
) {
31733197
return (
31743198
/** @type {WebSocketURL} */
31753199
(/** @type {ClientConfiguration} */ (this.options.client).webSocketURL)
3176-
.hostname === hostname
3200+
.hostname === value
31773201
);
31783202
}
31793203

3180-
// disallow
31813204
return false;
31823205
}
31833206

@@ -3198,6 +3221,64 @@ class Server {
31983221
}
31993222
}
32003223

3224+
/**
3225+
* @private
3226+
* @param {{ [key: string]: string | undefined }} headers
3227+
* @returns {boolean}
3228+
*/
3229+
isSameOrigin(headers) {
3230+
if (this.options.allowedHosts === "all") {
3231+
return true;
3232+
}
3233+
3234+
const originHeader = headers.origin;
3235+
3236+
if (!originHeader) {
3237+
return this.options.allowedHosts === "all";
3238+
}
3239+
3240+
if (DEFAULT_ALLOWED_PROTOCOLS.test(originHeader)) {
3241+
return true;
3242+
}
3243+
3244+
const origin = url.parse(originHeader, false, true).hostname;
3245+
3246+
if (origin === null) {
3247+
return false;
3248+
}
3249+
3250+
if (this.isHostAllowed(origin)) {
3251+
return true;
3252+
}
3253+
3254+
const hostHeader = headers.host;
3255+
3256+
if (!hostHeader) {
3257+
return this.options.allowedHosts === "all";
3258+
}
3259+
3260+
if (DEFAULT_ALLOWED_PROTOCOLS.test(hostHeader)) {
3261+
return true;
3262+
}
3263+
3264+
const host = url.parse(
3265+
// if hostHeader doesn't have scheme, add // for parsing.
3266+
/^(.+:)?\/\//.test(hostHeader) ? hostHeader : `//${hostHeader}`,
3267+
false,
3268+
true
3269+
).hostname;
3270+
3271+
if (host === null) {
3272+
return false;
3273+
}
3274+
3275+
if (this.isHostAllowed(host)) {
3276+
return true;
3277+
}
3278+
3279+
return origin === host;
3280+
}
3281+
32013282
/**
32023283
* @private
32033284
* @param {Request} req

test/e2e/allowed-hosts.test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,7 +1276,7 @@ describe("allowed hosts", () => {
12761276
waitUntil: "networkidle0",
12771277
});
12781278

1279-
if (!server.checkHeader(headers, "host")) {
1279+
if (!server.isValidHost(headers, "host")) {
12801280
throw new Error("Validation didn't fail");
12811281
}
12821282

@@ -1317,7 +1317,7 @@ describe("allowed hosts", () => {
13171317
waitUntil: "networkidle0",
13181318
});
13191319

1320-
if (!server.checkHeader(headers, "host")) {
1320+
if (!server.isValidHost(headers, "host")) {
13211321
throw new Error("Validation didn't fail");
13221322
}
13231323

@@ -1360,7 +1360,7 @@ describe("allowed hosts", () => {
13601360
waitUntil: "networkidle0",
13611361
});
13621362

1363-
if (!server.checkHeader(headers, "host")) {
1363+
if (!server.isValidHost(headers, "host")) {
13641364
throw new Error("Validation didn't fail");
13651365
}
13661366

@@ -1404,7 +1404,7 @@ describe("allowed hosts", () => {
14041404
waitUntil: "networkidle0",
14051405
});
14061406

1407-
if (!server.checkHeader(headers, "host")) {
1407+
if (!server.isValidHost(headers, "host")) {
14081408
throw new Error("Validation didn't fail");
14091409
}
14101410

@@ -1444,7 +1444,7 @@ describe("allowed hosts", () => {
14441444
waitUntil: "networkidle0",
14451445
});
14461446

1447-
if (!server.checkHeader(headers, "host")) {
1447+
if (!server.isValidHost(headers, "host")) {
14481448
throw new Error("Validation didn't fail");
14491449
}
14501450

@@ -1485,7 +1485,7 @@ describe("allowed hosts", () => {
14851485
tests.forEach((test) => {
14861486
const headers = { host: test };
14871487

1488-
if (!server.checkHeader(headers, "host")) {
1488+
if (!server.isValidHost(headers, "host")) {
14891489
throw new Error("Validation didn't fail");
14901490
}
14911491
});
@@ -1535,7 +1535,7 @@ describe("allowed hosts", () => {
15351535
tests.forEach((test) => {
15361536
const headers = { host: test };
15371537

1538-
if (!server.checkHeader(headers, "host")) {
1538+
if (!server.isValidHost(headers, "host")) {
15391539
throw new Error("Validation didn't fail");
15401540
}
15411541
});

test/e2e/api.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ describe("API", () => {
837837
tests.forEach((test) => {
838838
const headers = { host: test };
839839

840-
if (!server.checkHeader(headers, "host")) {
840+
if (!server.isValidHost(headers, "host")) {
841841
throw new Error("Validation didn't pass");
842842
}
843843
});
@@ -886,7 +886,7 @@ describe("API", () => {
886886
waitUntil: "networkidle0",
887887
});
888888

889-
if (!server.checkHeader(headers, "origin")) {
889+
if (!server.isValidHost(headers, "origin")) {
890890
throw new Error("Validation didn't fail");
891891
}
892892

0 commit comments

Comments
 (0)