HTTP host matching not working across TCP packet boundaries?

Hello,
I have configured a basic set of rules to allow http.host headers, and drop all else. The allow rule works properly up until (what seems to be) where the request body needs to be split into multiple TCP packets. My request body configuration settings should allow the matching, as it’s far below 4kb.

          request-body-minimal-inspect-size: 32kb
          request-body-inspect-window: 4kb

Rules:

pass http $HOME_NET any -> $EXTERNAL_NET any (http.host; content:"example.com"; msg:"matched example.com host header"; priority:1; sid:4;
drop http $HOME_NET any -> $EXTERNAL_NET any (msg:"Drop due to no match"; priority:1; sid:6;

Shell:

[~]$ echo $SHELL
/bin/bash

Example that works - (Small enough request to fit in 1 TCP packet for the request)

    [~]$ curl -Ikv --request GET 'http://example.com' --header "x-bravo-bravo: `for i in {1..120}; do echo -n \"BobsMyUncle\"; done;`" --output /dev/null
    * Rebuilt URL to: http://example.com/
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
      0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 93.184.216.34...
    * TCP_NODELAY set
    * Connected to example.com (93.184.216.34) port 80 (#0)
    > GET / HTTP/1.1
    > Host: example.com
    > User-Agent: curl/7.61.1
    > Accept: */*
    > x-bravo-bravo: BobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncle
    > 
    < HTTP/1.1 200 OK
    < Age: 573259
    < Cache-Control: max-age=604800
    < Content-Type: text/html; charset=UTF-8
    < Date: Tue, 22 Dec 2020 21:54:33 GMT
    < Etag: "3147526947+ident"
    < Expires: Tue, 29 Dec 2020 21:54:33 GMT
    < Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
    < Server: ECS (sec/96EE)
    < Vary: Accept-Encoding
    < X-Cache: HIT
    < Content-Length: 1256
    < 
    * Excess found in a non pipelined read: excess = 1256 url = / (zero-length body)
      0  1256    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
    * Connection #0 to host example.com left intact

Request that is just large enough to be over 2 TCP packets.

[~]$ curl -Ikv --request GET 'http://example.com' --header "x-bravo-bravo: `for i in {1..125}; do echo -n \"BobsMyUncle\"; done;`" --output /dev/null
* Rebuilt URL to: http://example.com/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 93.184.216.34...
* TCP_NODELAY set
* Connected to example.com (93.184.216.34) port 80 (#0)
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.61.1
> Accept: */*
> x-bravo-bravo: BobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncleBobsMyUncle
> 
  0     0    0     0    0     0      0      0 --:--:--  0:02:00 --:--:--     0^C

Suricata checks to see if the header is terminated with CRLF(0d0a0d0a) before checking the HTTP header values. If the packet lengthens with a cookie or other value, the CRLF is identified after the first packet. suricata checks that the HTTP header is terminated. It then compares the loaded rule with the stream and alerts if it matches.

Through the drop http ~ and pass http ~ rules mentioned above, we hope that example.com will pass and the rest of the request will be blocked. However, if the method and space are identified, such as GET + Space (0x20), it is considered as http. The header termination of an HTTP request with two or more packets split is checked after the first packet. Eventually, all HTTP requests are blocked as method and space are already checked before the rule where http.host is used is checked.

You can consider using it like this rule.

drop http $HOME_NET any -> $EXTERNAL_NET any (msg:"Excluding example.com"; http.host; content:!"example.com"; )

@bravo_bravo could you craft a suricata-verify test out of this ?