@@ -307,6 +307,8 @@ function useFn(route, fn) {
307
307
return /** @type {BasicApplication } */ ( { } ) ;
308
308
}
309
309
310
+ const DEFAULT_ALLOWED_PROTOCOLS = / ^ ( f i l e | .+ - e x t e n s i o n ) : / i;
311
+
310
312
/**
311
313
* @typedef {Object } BasicApplication
312
314
* @property {typeof useFn } use
@@ -1961,7 +1963,7 @@ class Server {
1961
1963
( req . headers ) ;
1962
1964
const headerName = headers [ ":authority" ] ? ":authority" : "host" ;
1963
1965
1964
- if ( this . checkHeader ( headers , headerName , true ) ) {
1966
+ if ( this . isValidHost ( headers , headerName ) ) {
1965
1967
next ( ) ;
1966
1968
return ;
1967
1969
}
@@ -2668,8 +2670,9 @@ class Server {
2668
2670
2669
2671
if (
2670
2672
! headers ||
2671
- ! this . checkHeader ( headers , "host" , true ) ||
2672
- ! this . checkHeader ( headers , "origin" , false )
2673
+ ! this . isValidHost ( headers , "host" ) ||
2674
+ ! this . isValidHost ( headers , "origin" ) ||
2675
+ ! this . isSameOrigin ( headers )
2673
2676
) {
2674
2677
this . sendMessage ( [ client ] , "error" , "Invalid Host/Origin header" ) ;
2675
2678
@@ -2703,7 +2706,8 @@ class Server {
2703
2706
2704
2707
if (
2705
2708
this . options . client &&
2706
- /** @type {ClientConfiguration } */ ( this . options . client ) . reconnect
2709
+ /** @type {ClientConfiguration } */
2710
+ ( this . options . client ) . reconnect
2707
2711
) {
2708
2712
this . sendMessage (
2709
2713
[ client ] ,
@@ -2718,9 +2722,9 @@ class Server {
2718
2722
/** @type {ClientConfiguration } */
2719
2723
( this . options . client ) . overlay
2720
2724
) {
2721
- const overlayConfig = /** @type { ClientConfiguration } */ (
2722
- this . options . client
2723
- ) . overlay ;
2725
+ const overlayConfig =
2726
+ /** @type { ClientConfiguration } */
2727
+ ( this . options . client ) . overlay ;
2724
2728
2725
2729
this . sendMessage (
2726
2730
[ client ] ,
@@ -3106,106 +3110,182 @@ class Server {
3106
3110
3107
3111
/**
3108
3112
* @private
3109
- * @param {{ [key: string]: string | undefined } } headers
3110
- * @param {string } headerToCheck
3111
- * @param {boolean } allowIP
3113
+ * @param {string } value
3112
3114
* @returns {boolean }
3113
3115
*/
3114
- checkHeader ( headers , headerToCheck , allowIP ) {
3116
+ isHostAllowed ( value ) {
3117
+ const { allowedHosts } = this . options ;
3118
+
3115
3119
// allow user to opt out of this security check, at their own risk
3116
3120
// by explicitly enabling allowedHosts
3121
+ if ( allowedHosts === "all" ) {
3122
+ return true ;
3123
+ }
3124
+
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 ) {
3117
3177
if ( this . options . allowedHosts === "all" ) {
3118
3178
return true ;
3119
3179
}
3120
3180
3121
3181
// get the Host header and extract hostname
3122
3182
// we don't care about port not matching
3123
- const hostHeader = headers [ headerToCheck ] ;
3183
+ const header = headers [ headerToCheck ] ;
3124
3184
3125
- if ( ! hostHeader ) {
3185
+ if ( ! header ) {
3126
3186
return false ;
3127
3187
}
3128
3188
3129
- if ( / ^ ( f i l e | . + - e x t e n s i o n ) : / i . test ( hostHeader ) ) {
3189
+ if ( DEFAULT_ALLOWED_PROTOCOLS . test ( header ) ) {
3130
3190
return true ;
3131
3191
}
3132
3192
3133
3193
// use the node url-parser to retrieve the hostname from the host-header.
3134
3194
const hostname = url . parse (
3135
- // if hostHeader doesn't have scheme, add // for parsing.
3136
- / ^ ( .+ : ) ? \/ \/ / . test ( hostHeader ) ? hostHeader : `//${ hostHeader } ` ,
3195
+ // if header doesn't have scheme, add // for parsing.
3196
+ / ^ ( .+ : ) ? \/ \/ / . test ( header ) ? header : `//${ header } ` ,
3137
3197
false ,
3138
3198
true ,
3139
3199
) . hostname ;
3140
3200
3141
- // allow requests with explicit IPv4 or IPv6-address if allowIP is true.
3142
- // Note that IP should not be automatically allowed for Origin headers,
3143
- // otherwise an untrusted remote IP host can send requests.
3144
- //
3201
+ if ( hostname === null ) {
3202
+ return false ;
3203
+ }
3204
+
3205
+ if ( this . isHostAllowed ( hostname ) ) {
3206
+ return true ;
3207
+ }
3208
+
3209
+ // always allow requests with explicit IPv4 or IPv6-address.
3145
3210
// A note on IPv6 addresses:
3146
- // hostHeader will always contain the brackets denoting
3211
+ // header will always contain the brackets denoting
3147
3212
// an IPv6-address in URLs,
3148
3213
// these are removed from the hostname in url.parse(),
3149
3214
// so we have the pure IPv6-address in hostname.
3150
3215
// For convenience, always allow localhost (hostname === 'localhost')
3151
3216
// and its subdomains (hostname.endsWith(".localhost")).
3152
3217
// allow hostname of listening address (hostname === this.options.host)
3153
3218
const isValidHostname =
3154
- ( allowIP &&
3155
- hostname !== null &&
3156
- ( ipaddr . IPv4 . isValid ( hostname ) || ipaddr . IPv6 . isValid ( hostname ) ) ) ||
3219
+ ipaddr . IPv4 . isValid ( hostname ) ||
3220
+ ipaddr . IPv6 . isValid ( hostname ) ||
3157
3221
hostname === "localhost" ||
3158
- ( hostname !== null && hostname . endsWith ( ".localhost" ) ) ||
3222
+ hostname . endsWith ( ".localhost" ) ||
3159
3223
hostname === this . options . host ;
3160
3224
3161
3225
if ( isValidHostname ) {
3162
3226
return true ;
3163
3227
}
3164
3228
3165
- const { allowedHosts } = this . options ;
3229
+ // disallow
3230
+ return false ;
3231
+ }
3166
3232
3167
- // always allow localhost host, for convenience
3168
- // allow if hostname is in allowedHosts
3169
- if ( Array . isArray ( allowedHosts ) && allowedHosts . length > 0 ) {
3170
- for ( let hostIdx = 0 ; hostIdx < allowedHosts . length ; hostIdx ++ ) {
3171
- /** @type {string } */
3172
- const allowedHost = allowedHosts [ hostIdx ] ;
3233
+ /**
3234
+ * @private
3235
+ * @param {{ [key: string]: string | undefined } } headers
3236
+ * @returns {boolean }
3237
+ */
3238
+ isSameOrigin ( headers ) {
3239
+ if ( this . options . allowedHosts === "all" ) {
3240
+ return true ;
3241
+ }
3173
3242
3174
- if ( allowedHost === hostname ) {
3175
- return true ;
3176
- }
3243
+ const originHeader = headers . origin ;
3177
3244
3178
- // support "." as a subdomain wildcard
3179
- // e.g. ".example.com" will allow "example.com", "www.example.com", "subdomain.example.com", etc
3180
- if ( allowedHost [ 0 ] === "." ) {
3181
- // "example.com" (hostname === allowedHost.substring(1))
3182
- // "*.example.com" (hostname.endsWith(allowedHost))
3183
- if (
3184
- hostname === allowedHost . substring ( 1 ) ||
3185
- /** @type {string } */ ( hostname ) . endsWith ( allowedHost )
3186
- ) {
3187
- return true ;
3188
- }
3189
- }
3190
- }
3245
+ if ( ! originHeader ) {
3246
+ return this . options . allowedHosts === "all" ;
3191
3247
}
3192
3248
3193
- // Also allow if `client.webSocketURL.hostname` provided
3194
- if (
3195
- this . options . client &&
3196
- typeof (
3197
- /** @type {ClientConfiguration } */ ( this . options . client ) . webSocketURL
3198
- ) !== "undefined"
3199
- ) {
3200
- return (
3201
- /** @type {WebSocketURL } */
3202
- ( /** @type {ClientConfiguration } */ ( this . options . client ) . webSocketURL )
3203
- . hostname === hostname
3204
- ) ;
3249
+ if ( DEFAULT_ALLOWED_PROTOCOLS . test ( originHeader ) ) {
3250
+ return true ;
3205
3251
}
3206
3252
3207
- // disallow
3208
- return false ;
3253
+ const origin = url . parse ( originHeader , false , true ) . hostname ;
3254
+
3255
+ if ( origin === null ) {
3256
+ return false ;
3257
+ }
3258
+
3259
+ if ( this . isHostAllowed ( origin ) ) {
3260
+ return true ;
3261
+ }
3262
+
3263
+ const hostHeader = headers . host ;
3264
+
3265
+ if ( ! hostHeader ) {
3266
+ return this . options . allowedHosts === "all" ;
3267
+ }
3268
+
3269
+ if ( DEFAULT_ALLOWED_PROTOCOLS . test ( hostHeader ) ) {
3270
+ return true ;
3271
+ }
3272
+
3273
+ const host = url . parse (
3274
+ // if hostHeader doesn't have scheme, add // for parsing.
3275
+ / ^ ( .+ : ) ? \/ \/ / . test ( hostHeader ) ? hostHeader : `//${ hostHeader } ` ,
3276
+ false ,
3277
+ true ,
3278
+ ) . hostname ;
3279
+
3280
+ if ( host === null ) {
3281
+ return false ;
3282
+ }
3283
+
3284
+ if ( this . isHostAllowed ( host ) ) {
3285
+ return true ;
3286
+ }
3287
+
3288
+ return origin === host ;
3209
3289
}
3210
3290
3211
3291
/**
0 commit comments