@@ -250,6 +250,8 @@ const encodeOverlaySettings = (setting) =>
250
250
? encodeURIComponent ( setting . toString ( ) )
251
251
: setting ;
252
252
253
+ const DEFAULT_ALLOWED_PROTOCOLS = / ^ ( f i l e | .+ - e x t e n s i o n ) : / i;
254
+
253
255
class Server {
254
256
/**
255
257
* @param {Configuration | Compiler | MultiCompiler } options
@@ -2011,7 +2013,7 @@ class Server {
2011
2013
*/
2012
2014
( req , res , next ) => {
2013
2015
if (
2014
- this . checkHeader (
2016
+ this . isValidHost (
2015
2017
/** @type {{ [key: string]: string | undefined } } */
2016
2018
( req . headers ) ,
2017
2019
"host" ,
@@ -2222,6 +2224,14 @@ class Server {
2222
2224
const headers =
2223
2225
/** @type {{ [key: string]: string | undefined } } */
2224
2226
( req . headers ) ;
2227
+
2228
+ const headerName = headers [ ":authority" ] ? ":authority" : "host" ;
2229
+
2230
+ if ( this . isValidHost ( headers , headerName , false ) ) {
2231
+ next ( ) ;
2232
+ return ;
2233
+ }
2234
+
2225
2235
if (
2226
2236
headers [ "sec-fetch-mode" ] === "no-cors" &&
2227
2237
headers [ "sec-fetch-site" ] === "cross-site"
@@ -2625,8 +2635,8 @@ class Server {
2625
2635
2626
2636
if (
2627
2637
! 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 )
2630
2640
) {
2631
2641
this . sendMessage ( [ client ] , "error" , "Invalid Host/Origin header" ) ;
2632
2642
@@ -3082,80 +3092,93 @@ class Server {
3082
3092
* @private
3083
3093
* @param {{ [key: string]: string | undefined } } headers
3084
3094
* @param {string } headerToCheck
3085
- * @param {boolean } allowIP
3095
+ * @param {boolean } validateHost
3086
3096
* @returns {boolean }
3087
3097
*/
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 ) {
3091
3099
if ( this . options . allowedHosts === "all" ) {
3092
3100
return true ;
3093
3101
}
3094
3102
3095
3103
// get the Host header and extract hostname
3096
3104
// we don't care about port not matching
3097
- const hostHeader = headers [ headerToCheck ] ;
3105
+ const header = headers [ headerToCheck ] ;
3098
3106
3099
- if ( ! hostHeader ) {
3107
+ if ( ! header ) {
3100
3108
return false ;
3101
3109
}
3102
3110
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 ) ) {
3104
3112
return true ;
3105
3113
}
3106
3114
3107
3115
// use the node url-parser to retrieve the hostname from the host-header.
3108
3116
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 } ` ,
3111
3119
false ,
3112
3120
true
3113
3121
) . hostname ;
3114
3122
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.
3119
3132
// A note on IPv6 addresses:
3120
- // hostHeader will always contain the brackets denoting
3133
+ // header will always contain the brackets denoting
3121
3134
// an IPv6-address in URLs,
3122
3135
// these are removed from the hostname in url.parse(),
3123
3136
// so we have the pure IPv6-address in hostname.
3124
3137
// For convenience, always allow localhost (hostname === 'localhost')
3125
3138
// and its subdomains (hostname.endsWith(".localhost")).
3126
3139
// 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
+ }
3138
3150
3151
+ /**
3152
+ * @private
3153
+ * @param {string } value
3154
+ * @returns {boolean }
3155
+ */
3156
+ isHostAllowed ( value ) {
3139
3157
const { allowedHosts } = this . options ;
3140
3158
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
+
3141
3165
// always allow localhost host, for convenience
3142
- // allow if hostname is in allowedHosts
3166
+ // allow if value is in allowedHosts
3143
3167
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 ) {
3148
3170
return true ;
3149
3171
}
3150
3172
3151
3173
// support "." as a subdomain wildcard
3152
3174
// 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))
3156
3178
if (
3157
- hostname === allowedHost . substring ( 1 ) ||
3158
- /** @type {string } */ ( hostname ) . endsWith ( allowedHost )
3179
+ value === allowedHost . substring ( 1 ) ||
3180
+ /** @type {string } */
3181
+ ( value ) . endsWith ( allowedHost )
3159
3182
) {
3160
3183
return true ;
3161
3184
}
@@ -3167,17 +3190,17 @@ class Server {
3167
3190
if (
3168
3191
this . options . client &&
3169
3192
typeof (
3170
- /** @type {ClientConfiguration } */ ( this . options . client ) . webSocketURL
3193
+ /** @type {ClientConfiguration } */
3194
+ ( this . options . client ) . webSocketURL
3171
3195
) !== "undefined"
3172
3196
) {
3173
3197
return (
3174
3198
/** @type {WebSocketURL } */
3175
3199
( /** @type {ClientConfiguration } */ ( this . options . client ) . webSocketURL )
3176
- . hostname === hostname
3200
+ . hostname === value
3177
3201
) ;
3178
3202
}
3179
3203
3180
- // disallow
3181
3204
return false ;
3182
3205
}
3183
3206
@@ -3198,6 +3221,64 @@ class Server {
3198
3221
}
3199
3222
}
3200
3223
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
+
3201
3282
/**
3202
3283
* @private
3203
3284
* @param {Request } req
0 commit comments