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>

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 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 security::provided_host_valid security::provided_host_valid (private) security::validated_host_header->security::provided_host_valid

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

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

    if {![string match *//* $host]} {
        set splithost [ns_conn protocol]://$host
    } else {
        set splithost $host
    }
    if {![util::split_location $splithost .proto hostName hostPort]} {
        return ""
    }

    #
    # 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 $hostName .]

    #
    # Check, if the provided host is the same as the configured host
    # name for the current driver or one of its IP addresses. Should
    # be true in most cases.
    #
    set driverInfo [util_driver_info]
    set driverHostName [dict get $driverInfo hostname]

    #
    # The port is currently ignored for determining the validated host
    # header field.
    #
    # Validation is OK, when the provided host-header content is
    # either the same as configured hostname in the driver
    # configuration or one of its IP addresses.
    #
    set validationOk 0
    if {$hostName eq $driverHostName} {
        set validationOk 1
    } else {
        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).
    #
    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.
    #
    if {$validationOk == 0 && [util::split_location [ad_url] .proto systemHost systemPort]} {
        set validationOk [expr {$hostName eq $systemHost
                                && ($hostPort eq $systemPort || $hostPort eq "") }]
    }

    if {$validationOk == 0 && [ns_info name] eq "NaviServer"} {
        #
        # 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"
                    set validationOk 1
                    break
                }
            }
        }
    }

    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} {
        #
        # This is not an attempt, where someone tries to lure us to a
        # different host via redirect. "localhost" is always safe.
        #
        set validationOk [expr {$hostName eq "localhost"}]
    }

    #
    # 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 $host
    }

    #
    # We could/should check as well against a white-list of additional
    # hostnames (maybe via ::acs::validated, or via config file, or
    # via additional package parameter). Probably the best way is to
    # get alternate (alias) names from the driver section of the
    # current driver [ns_conn driver] (maybe check global and local).
    #
    #ns_set array [ns_configsection ns/module/nssock/servers]

    #
    # Now we give up
    
            set info ""
            foreach {k v} [ns_set array [ns_conn headers]] {
                append info "\n $k:\t$v"
            }
            append info "\n[ns_conn method] [ns_conn url]"
    ns_log warning "ignore untrusted host header field: '$host'$info"
    #ad_log warning "ignore untrusted host header field: '$host'"

    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: