@@ -250,6 +250,8 @@ const encodeOverlaySettings = (setting) =>
250250 ? encodeURIComponent ( setting . toString ( ) )
251251 : setting ;
252252
253+ const DEFAULT_ALLOWED_PROTOCOLS = / ^ ( f i l e | .+ - e x t e n s i o n ) : / i;
254+
253255class 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 ( / ^ ( f i l e | . + - e x t e n s i o n ) : / 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
0 commit comments