@@ -307,6 +307,8 @@ function useFn(route, fn) {
307307 return /** @type {BasicApplication } */ ( { } ) ;
308308}
309309
310+ const DEFAULT_ALLOWED_PROTOCOLS = / ^ ( f i l e | .+ - e x t e n s i o n ) : / 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 , true ) ) {
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" , true ) ||
2672- ! this . checkHeader ( headers , "origin" , false )
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
@@ -2703,7 +2706,8 @@ class Server {
27032706
27042707 if (
27052708 this . options . client &&
2706- /** @type {ClientConfiguration } */ ( this . options . client ) . reconnect
2709+ /** @type {ClientConfiguration } */
2710+ ( this . options . client ) . reconnect
27072711 ) {
27082712 this . sendMessage (
27092713 [ client ] ,
@@ -2718,9 +2722,9 @@ class Server {
27182722 /** @type {ClientConfiguration } */
27192723 ( this . options . client ) . overlay
27202724 ) {
2721- const overlayConfig = /** @type { ClientConfiguration } */ (
2722- this . options . client
2723- ) . overlay ;
2725+ const overlayConfig =
2726+ /** @type { ClientConfiguration } */
2727+ ( this . options . client ) . overlay ;
27242728
27252729 this . sendMessage (
27262730 [ client ] ,
@@ -3106,106 +3110,182 @@ class Server {
31063110
31073111 /**
31083112 * @private
3109- * @param {{ [key: string]: string | undefined } } headers
3110- * @param {string } headerToCheck
3111- * @param {boolean } allowIP
3113+ * @param {string } value
31123114 * @returns {boolean }
31133115 */
3114- checkHeader ( headers , headerToCheck , allowIP ) {
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
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 ) {
31173177 if ( this . options . allowedHosts === "all" ) {
31183178 return true ;
31193179 }
31203180
31213181 // get the Host header and extract hostname
31223182 // we don't care about port not matching
3123- const hostHeader = headers [ headerToCheck ] ;
3183+ const header = headers [ headerToCheck ] ;
31243184
3125- if ( ! hostHeader ) {
3185+ if ( ! header ) {
31263186 return false ;
31273187 }
31283188
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 ) ) {
31303190 return true ;
31313191 }
31323192
31333193 // use the node url-parser to retrieve the hostname from the host-header.
31343194 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 } ` ,
31373197 false ,
31383198 true ,
31393199 ) . hostname ;
31403200
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.
31453210 // A note on IPv6 addresses:
3146- // hostHeader will always contain the brackets denoting
3211+ // header will always contain the brackets denoting
31473212 // an IPv6-address in URLs,
31483213 // these are removed from the hostname in url.parse(),
31493214 // so we have the pure IPv6-address in hostname.
31503215 // For convenience, always allow localhost (hostname === 'localhost')
31513216 // and its subdomains (hostname.endsWith(".localhost")).
31523217 // allow hostname of listening address (hostname === this.options.host)
31533218 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 ) ||
31573221 hostname === "localhost" ||
3158- ( hostname !== null && hostname . endsWith ( ".localhost" ) ) ||
3222+ hostname . endsWith ( ".localhost" ) ||
31593223 hostname === this . options . host ;
31603224
31613225 if ( isValidHostname ) {
31623226 return true ;
31633227 }
31643228
3165- const { allowedHosts } = this . options ;
3229+ // disallow
3230+ return false ;
3231+ }
31663232
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+ }
31733242
3174- if ( allowedHost === hostname ) {
3175- return true ;
3176- }
3243+ const originHeader = headers . origin ;
31773244
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" ;
31913247 }
31923248
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 ;
32053251 }
32063252
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 ;
32093289 }
32103290
32113291 /**
0 commit comments