security::validated_host_header (public)

 security::validated_host_header

Defined in packages/acs-tcl/tcl/security-procs.tcl

Returns:
validated host header field or empty
Author:
Gustaf Neumann Protect against faked or invalid host header fields. Host header attacks can lead to web-cache poisoning and password reset attacks (for more details, see e.g. http://www.skeletonscribe.net/2013/05/practical-http-host-header-attacks.html) or to unintended redirects to different sites. The validated host header most be syntactically correct, and it must be either configured/white-listed or it must be from a non-routable IP address. White-listed hosts are taken from the alternate host names specified in the "ns/module/DRIVER/servers" section, or via the configuration variable "hostname" (e.g., "openacs.org www.openacs.org") which is added the the "/server" section during startup.

Partial Call Graph (max 5 caller/called nodes):
%3 util_current_location util_current_location (public) security::validated_host_header security::validated_host_header util_current_location->security::validated_host_header acs::icanuse acs::icanuse (public) security::validated_host_header->acs::icanuse ad_conn ad_conn (public) security::validated_host_header->ad_conn ad_url ad_url (public) security::validated_host_header->ad_url db_0or1row db_0or1row (public) security::validated_host_header->db_0or1row security::configured_driver_info security::configured_driver_info (public) security::validated_host_header->security::configured_driver_info

Testcases:
No testcase defined.
Source code:
    #
    # Check, if we have a host header field
    #
    set hostHeaderValue [ns_set iget [ns_conn headers] Host]
    if {$hostHeaderValue eq ""} {
        return ""
    }
    #
    # Domain names are case insensitive. So convert it to lower to
    # avoid surprises.
    #
    set hostHeaderValue [string tolower $hostHeaderValue]

    #
    # Check, if we have validated it before, or it belongs to the
    # predefined accepted host header fields.
    #
    set key ::acs::validated_host_header($hostHeaderValue)
    if {[info exists $key]} {
        return $hostHeaderValue
    }

    set hostHeaderDict [ns_parsehostport $hostHeaderValue]
    #
    # Remove trailing dot, as this is allowed in fully qualified DNS
    # names (see e.g. §3.2.2 of RFC 3976).
    #
    set hostName [string trimright [dict get $hostHeaderDict host] .]
    set hostPort [expr {[dict exists $hostHeaderDict port] ? [dict get $hostHeaderDict port] : ""}]

    set normalizedHostHeaderValue [util::join_location -host $hostName -port $hostPort]
    set validationOk 0

    #
    # Check if the value in "hostName" can be regarded as safe.
    #
    # The host header value is one of the names registered for
    # this server.
    #
    if {[acs::icanuse "ns_server hosts"]} {
        if {$normalizedHostHeaderValue in [ns_server hosts]} {
            #
            #
            set validationOk 1
        }
    } elseif {[ns_info name] eq "NaviServer"} {
        #
        # As a replacement for "ns_server hosts" check against the
        # virtual server configuration of NaviServer.
        #
        set s [ns_info server]
        set driverInfo [security::configured_driver_info]
        set drivers [lmap d $driverInfo {dict get $d driver}]

        foreach driver $drivers {
            #
            # Check global "servers" configuration for virtual servers for the driver
            #
            set ns [ns_configsection ns/module/$driver/servers]
            if {$ns ne ""} {
                #
                # We have a global "servers" configuration for the driver
                #
                set names [lmap {key value} [ns_set array $ns] {
                    if {$key ne $s} continue
                    set value
                }]
                if {$host in $names} {
                    ns_log notice "security::validated_host_header: found $host"  "in global virtual server configuration for $driver"
                    return 1
                }
            }
        }
    }

    if {$validationOk == 0} {
        set validationOk [security::secure_hostname_p $hostName]
    }

    if {$validationOk == 0} {
        #
        # Check against the white-listed hosts from
        #
        #     ns_section ns/server/$server/acs {
        #         ns_param whitelistedHosts {...}
        #     }
        #
        # of the configuration file.
        #
        if {$hostHeaderValue in [ns_config "ns/server/[ns_info server]/acs" whitelistedHosts {}]} {
            set validationOk 1
        }
    }

    if {$validationOk == 0} {
        #
        # Check against host node map. Here we need as well protection
        # against invalid utf-8 characters.
        #
        if {![security::provided_host_valid $hostName]} {
            return ""
        }

        set validationOk [db_0or1row host_header_field_mapped {
            select 1 from host_node_map where host = :hostName
        }]
    }

    if {$validationOk == 0} {
        #
        # Validation is OK, when the hostName is either the same as
        # configured hostname. This is a legacy branch for very old
        # versions of NaviServer or AOLserver.
        #
        set driverInfo [util_driver_info]
        set driverHostName [dict get $driverInfo hostname]
        if {$hostName eq $driverHostName} {
            set validationOk 1
        }
    }
    if {$validationOk == 0 && [info exists driverHostName]} {
        #
        # Validation is OK, when the hostName is one of the IP
        # addresses of the configured host name.
        #
        try {
            ns_addrbyhost -all $driverHostName
        } on error {errorMsg} {
            #
            # Name resolution of the hostname configured for this
            # driver failed, we cannot validate incoming IP addresses.
            #
            ns_log error "security::validated_host_header: configuration error:"  "name resolution for configured hostname '$driverHostName'"  "of driver '[ad_conn driver]' failed"
        } on ok {result} {
            set validationOk [expr {$hostName in $result}]
        }
    }

    #
    # Check, if the provided host is the same in [ns_conn location]
    # (will be used as default, but we do not want a warning in such
    # cases). This is also a legacy case.
    #
    if {$validationOk == 0
        && [util::split_location [ns_conn location] proto locationHost locationPort]} {
        set validationOk [expr {$hostName eq $locationHost}]
    }

    #
    # Check, if the provided host is the same as in the configured
    # SystemURL. Legacy case.
    #
    if {$validationOk == 0 && [util::split_location [ad_url] .proto systemHost systemPort]} {
        set validationOk [expr {$hostName eq $systemHost
                                && ($hostPort eq $systemPort || $hostPort eq "") }]
    }


    #
    # When any of the validation attempts above were successful, we
    # are done. We keep the logic for successful lookups
    # centralized. Performance of the individual tests are not
    # critical, since the lookups are cache per thread.
    #
    if {$validationOk} {
        set $key 1
        return $hostHeaderValue
    }


    #
    # Now we give up
    #
    ns_log warning "ignore untrusted host header field: '$hostHeaderValue'."  "Consider adding this value to 'whitelistedHosts' in the"  "section 'ns/server/$server/acs' of your configuration file"

    return ""
Generic XQL file:
packages/acs-tcl/tcl/security-procs.xql

PostgreSQL XQL file:
packages/acs-tcl/tcl/security-procs-postgresql.xql

Oracle XQL file:
packages/acs-tcl/tcl/security-procs-oracle.xql

[ hide source ] | [ make this the default ]
Show another procedure: