Forum Discussion

Chronos_258816's avatar
Chronos_258816
Icon for Nimbostratus rankNimbostratus
Jul 28, 2016

Host Header Validation

Greetings,

 

I have been tasked with crafting an iRule to validate the host header of incoming packets to a given virtual server. I have tried a few different irules to attempt this an yet I am told that the security team is able to modify the host header of the packet and still get around the iRule's checks. Would someone be able to look over the iRule's and tell me what I may be missing/doing wrong?

 

when HTTP_REQUEST if { [HTTP::host] equals "xyz.com"}{ pool /Common/xyz_pool }else{ reject } }

 

(this one below is more an attempt to force the correct one than validate whats coming in)

 

when HTTP_REQUEST { if {[HTTP::header exists Host]}{ HTTP::Header replace Host xyz.com }else{ HTTP::header replace Host xyz.com } }

 

  • A data-group is a convenience rather than a necessity.

    I tried the following on 11.5.4:

     

     

    when HTTP_REQUEST {
        if { [HTTP::host] ne "xyz.com" } {
            reject
        }
    }
    

     

     

    I tried all of the following combinations:

    1. HTTP/1.1 Host header xyz.com;
    2. HTTP/1.1 Host header foo.com;
    3. HTTP/1.0 no Host header;
    4. HTTP/1.1 Host header xyz.com followed by HTTP/1.0 no Host header;
    5. HTTP/1.1 Host header xyz.com followed by HTTP/1.1 Host header foo.com

    Case 1: allowed;

    Case 2: rejected (i.e., TCP RST);

    Case 3: rejected;

    Case 4: allowed then rejected;

    Case 5: allowed then rejected.

    Incidentally, if a pool is assigned to the VS, then the else clause isn't needed (it's the default anyway).

    Having said all of that, your issue may be relate to this note, found in the reject explanation:

     

    Subsequent code in the current event in the current 
    iRule or other iRules on the VS are still executed 
    prior to the reset being sent.
    

     

    I recommend putting a return after the reject.

     

  • You need an datagroup that is essentially a whitelist. Host headers are user submitted, so the user can manipulate what is sent in. Try something like this..

    datagroup would be a string type and would contain something like this:

    www.example.com := www.example.com_pool

     

        when HTTP_REQUEST {
                set hostpool [class match -value [string tolower [HTTP::host]] equals datagroup_name
                if { $hostpool ne "" } {
                                pool $hostpool
                                HTTP::header replace Host [HTTP::host]
                } else {
                                discard
                }
         }
    

     

  • A data-group is a convenience rather than a necessity.

    I tried the following on 11.5.4:

     

     

    when HTTP_REQUEST {
        if { [HTTP::host] ne "xyz.com" } {
            reject
        }
    }
    

     

     

    I tried all of the following combinations:

    1. HTTP/1.1 Host header xyz.com;
    2. HTTP/1.1 Host header foo.com;
    3. HTTP/1.0 no Host header;
    4. HTTP/1.1 Host header xyz.com followed by HTTP/1.0 no Host header;
    5. HTTP/1.1 Host header xyz.com followed by HTTP/1.1 Host header foo.com

    Case 1: allowed;

    Case 2: rejected (i.e., TCP RST);

    Case 3: rejected;

    Case 4: allowed then rejected;

    Case 5: allowed then rejected.

    Incidentally, if a pool is assigned to the VS, then the else clause isn't needed (it's the default anyway).

    Having said all of that, your issue may be relate to this note, found in the reject explanation:

     

    Subsequent code in the current event in the current 
    iRule or other iRules on the VS are still executed 
    prior to the reset being sent.
    

     

    I recommend putting a return after the reject.

     

    • Chronos_258816's avatar
      Chronos_258816
      Icon for Nimbostratus rankNimbostratus

      Good to know that it should be working as intended as per your examples above. I have placed the return after the reject and hope that takes care of the issue. If not, I need to see just how the exploits are being done.

       

    • ekaleido's avatar
      ekaleido
      Icon for Cirrus rankCirrus

      Curious because I'm not near a VE to test on, but is the following case sensitive:

       

      [HTTP::host] ne "xyz.com"

       

      As in, could Xyz.com create a different behavior?

       

    • VernonWells's avatar
      VernonWells
      Icon for Employee rankEmployee

      Naturally, but actually, that snippet is "more secure", in the sense that it is stricter. That is, if the Host header is Xyz.com (which, by RFC, means exactly the same thing as xyz.com), then it will be rejected. To allow any case (which, again, would be the RFC conforming choice):

       

      if { [string tolower [HTTP::host]] ne "xyz.com" } { reject }
      

       

  • Are you using OneConnect ? By default F5 makes load balancing decision based on the very 1st HTTP request and once a pool member is selected, subsequent HTTP requests within the same TCP connection is sent to the same pool member. You can force the F5 to make a load balancing decision per HTTP Request instead of the default per TCP connection by using OneConnect.

     

  • HTTP_REQUEST should fire on each received HTTP Request message (strictly speaking, once the HTTP Request headers are fully received) regardless of the OneConnect setting. To demonstrate this, I use the following rule:

     

    when HTTP_REQUEST {
        log local0. "Received Host header: [HTTP::host]"
    }
    

     

    On a VS without OneConnect, I do the following:

    1. Single request for Host = aaa.com in single connection; then
    2. Request for bbb.com, then ccc.com, then ddd.com in a single connection.

    This is what the log looks like after these two connections, which collectively include four requests:

     

    Rule /Common/rule-validate-host : Received Host header: aaa.com
    Rule /Common/rule-validate-host : Received Host header: bbb.com
    Rule /Common/rule-validate-host : Received Host header: ccc.com
    Rule /Common/rule-validate-host : Received Host header: ddd.com
    

     

    Then I apply OneConnect to the same VS and repeat the tests. The logs:

     

    Rule /Common/rule-validate-host : Received Host header: aaa.com
    Rule /Common/rule-validate-host : Received Host header: bbb.com
    Rule /Common/rule-validate-host : Received Host header: ccc.com
    Rule /Common/rule-validate-host : Received Host header: ddd.com
    

     

    The long-and-short: HTTP_REQUEST fires on each Request message regardless of Keep-Alive and OneConnect.