feat: adding new rule 920341 to avoid content-type evasion on HTTP/2#4341
feat: adding new rule 920341 to avoid content-type evasion on HTTP/2#4341touchweb-vincent wants to merge 13 commits into
Conversation
|
📊 Quantitative test results for language: |
|
Can you point to a RFC? Thanks. |
|
In the older RFC 2616 https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html, it is stated: "The presence of a message-body in a request is signaled by the inclusion of a Content-Length or Transfer-Encoding header field in the request's message-headers." The newer RFC 9112 https://httpwg.org/specs/rfc9112.html#rfc.section.6.1 includes this section: "The Transfer-Encoding header field lists the transfer coding names corresponding to the sequence of transfer codings that have been (or will be) applied to the content in order to form the message body." It is never explicitly stated that a Transfer-Encoding header necessarily implies the presence of a body - however, this has always been observed in practice. I’ve never seen a single HTTP frame without a body that still included a Transfer-Encoding header. It is only stated that a body must always be announced through either a Content-Length or a Transfer-Encoding header. That said, don’t ask me why an HTTP header specific to HTTP/1.1 is still allowed under the HTTP/2 protocol and causes this kind of issue. I assume it’s a tolerance that was kept for backward compatibility. |
|
Can you explain more what kind of issues? What exactly are you trying to fix? |
|
This must be blocked - which is not the case for the moment.
More details on the other one: #4339 |
|
|
What exactly you want to be blocked? As i said, you need to describe it more, it is not clear. |
| # https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html | ||
| # | ||
|
|
||
| SecRule &REQUEST_HEADERS:Transfer-Encoding "!@eq 0" \ |
There was a problem hiding this comment.
This rule should be scoped to HTTP/2, right?
There was a problem hiding this comment.
No, the rule must cover all versions of the HTTP protocol. It’s not impossible that this may also concern future versions of HTTP, including HTTP/3.
If there is a BODY (so a Content-length or Transfer-Encoding header), there must be a Content-Type header; otherwise, various bypasses become possible, effectively neutralizing most WAF rules on inputs that rely on it (at minimum JSON and XML).
We have more rules on this topic here - I will propose additional PRs soon.
There was a problem hiding this comment.
In that case, please adjust the rule comment. The way it is written now it suggests that rule only applies to HTTP/2 traffic.
There was a problem hiding this comment.
No, the rule must cover all versions of the HTTP protocol. It’s not impossible that this may also concern future versions of HTTP, including HTTP/3.
I tried to understand this rule and read docs about this header. Found this:
Warning: HTTP/2 disallows all uses of the
Transfer-Encodingheader. HTTP/2 and later provide more efficient mechanisms for data streaming than chunked transfer. Usage of the header in HTTP/2 may likely result in a specificprotocol error.
Also in your referenced RFC above in rule's documentation in section 8.2.2 (the link points there) I found this:
8.2.2. Connection-Specific Header Fields
HTTP/2 does not use the Connection header field (Section 7.6.1 of [HTTP]) to indicate connection-specific header fields; in this protocol, connection-specific metadata is conveyed by other means. An endpoint MUST NOT generate an HTTP/2 message containing connection-specific header fields. This includes the Connection header field and those listed as having connection-specific semantics in Section 7.6.1 of [HTTP] (that is, Proxy-Connection, Keep-Alive, Transfer-Encoding, and Upgrade). Any message containing connection-specific header fields MUST be treated as malformed (Section 8.1.1).
As I know here the endpoint regards to both side of a connection, so I think this means that a client MUST NOT send the Transfer-Encoding field in case of HTTP/2.
There was a problem hiding this comment.
A client can send whatever it wants - it is out of our control. It is up to the server to block it, or up to the WAF to compensate for it, as we have always done within the OWASP CRS project.
The RFC states: “Any message containing connection-specific header fields MUST be treated as malformed.”
Currently, neither Apache2 nor Nginx (when not configured as a reverse proxy) treats this as “malformed,” assuming that “malformed” implies that it must be blocked.
There was a problem hiding this comment.
A client can send whatever it wants - it is out of our control. It is up to the server to block it, or up to the WAF to compensate for it, as we have always done within the OWASP CRS project.
Yes, exactly. And I think a WAF's task is to block malicious and malformed requests.
Currently, neither Apache2 nor Nginx (when not configured as a reverse proxy) treats this as “malformed,” assuming that “malformed” implies that it must be blocked.
THANK YOU!
Now I think it's time to write a rule which blocks any HTTP/2 or greater version request if it contains a Transfer-Encoding header. (And not write a rule which allows that, even if the server allows it.)
Regarding to your proposed solution: your rule does not protect the application if the request's version is HTTP/2 (or greater) - now I checked it with HTTP/2. The mentioned payload passed through the WAF (yes, with a HTTP/2 request, not with HTTP/1.1 or lower:
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x55ef0a1e4350)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> POST /script.php HTTP/2
> Host: my.server.com
> user-agent: curl/7.81.0
> accept: */*
).
So now I don't see what's your aim with this rule.
There was a problem hiding this comment.
Yes, this rule is a first building block. We discussed with Dan on Slack that it was not sufficient for Nginx, which is why a second building block was added to consolidate the whole. You can find it here: https://github.com/coreruleset/coreruleset/pull/4347/files
There was a problem hiding this comment.
Now I think it's time to write a rule which blocks any HTTP/2 or greater version request if it contains a
Transfer-Encodingheader. (And not write a rule which allows that, even if the server allows it.)
I did consider this, but I assumed that the tolerance maintained by the Apache2 and Nginx developers is most likely due to backward-compatibility considerations. That said, it would clearly be the best solution.
There was a problem hiding this comment.
We have multiple rules covering this area here. One of them also addresses this requirement by blocking PATCH, POST, and PUT requests lacking both a Content-Length and a Content-Type header.
|
@touchweb-vincent As you don't want to explain yourself, i can only guess what are you up to. So, see this: If you meant this (not sure, just quessing) then it is caused by web server, which is stripping off the body in case of empty |
|
I’m sorry, but I genuinely don’t understand your sentence. It’s not that I “don’t want to explain myself” - it’s simply that I don’t see what, in this context, would require any additional explanation. I strongly recommend not using the CRS sandbox for this type of test. Instead, please use one of your own servers and reproduce the issue properly. Here is how to do it:
string(33) "{"id_order":"select(sleep(10));"}" --84bc9111-B-- --84bc9111-C-- This proves that it is entirely possible, without a Content-Type or Content-Length header, to send a functional BODY on a standard Apache2 setup with HTTP/2 - tested on Debian 12 with version 2.4.65. This will allow you to observe the real behaviour without interference from the sandbox environment which do no manage H2 properly for the moment. |
|
Of course i tested it also on real server. What i said is correct. |
|
I don’t see, in any of the tests you included in your previous comment, this exact use case: You tested without Content-Type, and you tested without Content-Length - but not without both.
Are you able to reproduce what I described in my previous comment by following the exact command lines provided? |
I also tested it directly with modsecurity. |
|
This is not my use case @azurit - test my use case please. |
|
I did exactly the same as you. If not, then you are not clear enough in what you are up to, as i said it (X times) already. |
|
Explain what is wrong/different with my tests. |
|
You don't write this : curl -v 'https://my_domain/my_script.php' Or this to take your example : curl -v 'https://my_domain/my_script.php' -d '{"id_order":"select(sleep(10));"}' -H 'Content-Type:' -H 'Content-Length:' --http2 You write this :
I don’t understand why you don’t see the difference between my example and yours. Is there a GitHub bug that strips things ? |
|
So tell me the difference. Do you mean |
|
I’m giving you a precise use case - you refuse to use it. And then you tell me it doesn’t work by producing different use cases that have nothing to do with mine. I don’t understand what further explanation you’re expecting - I assume the differences are obvious, given that these are not the same use cases. I don’t understand the purpose of this line of reasoning. |
|
Because there is not difference if i use |
|
|
Then it means this use case does not work on all environments. It has been tested and validated here on: Debian 11 – Apache2 2.4.65 – curl/7.74.0 – Apache2 as the frontend Do you have a proxy in front of Apache2? Which versions of Apache2 and curl are you using? |
|
Debian 11, Apache 2.4.65, curl 7.74.0. No proxy. |
|
For the POC to work, HTTP/2 must be handled properly - are you sure the server accepts it and is capable of processing it? The POC does not seem to work on Docker images tested by partners - the call is downgraded to HTTP/1.1, which can be seen in the ModSecurity2 audit log. The POC also does not work on Nginx when it is configured as a reverse proxy in front of Apache2, as it detects a corrupted request. However, it does work when Nginx is not acting as a reverse proxy (standalone Nginx + FPM stack). It has been tested and validated by two fellow hosting providers on production servers. |
But WHAT was validated? I still don't understand this whole 'issue'. |
|
Since this is an unintended behaviour on HTTP/2, it’s possible that a more recent version of curl has disabled it. I assume that with this version of curl, you no longer see the Transfer-Encoding: chunked in the audit log. Could you downgrade to the native curl version on Debian 11/12 and run your tests again, please? |
|
Please describe the issue first. This is not about curl but about web server discarding the body of invalid requests. |
|
It has been confirmed that the BODY is not removed - meaning the payload does reach its destination (the FPM pools). The same payload can therefore bypass the XML/JSON processors, effectively neutralizing a large number of security rules. |
|
It is even not getting into the modsecurity, i checked that. |
|
Sorry, i'm not going to spend more time on this. There is either no issue or you are not able to describe it properly. |
|
The issue is that this payload - a content-type evasion technique - allows an attacker to target any PHP endpoint that accepts this as input via file_get_contents('php://input');. And currently, nothing within the CRS prevents its injection. This is a severe to critical security problem, as it allows bypassing a large number of security rules on endpoints that are very common across the PHP ecosystem. The payload is fully functional - tested and validated by two fellow providers, meaning thousands of servers - and it effectively neutralizes nearly all cyber-defense mechanisms. The issue isn’t that I’m explaining it poorly - the issue is that you’re unable to reproduce the POC and therefore can’t fully grasp the severity of the problem. This means there are race conditions affecting this POC. Which I hadn’t realized before you told me you couldn’t reproduce the POC.
Our POC work on curl/8.14.1 (Debian 12 backport) as well as native Debian version. With a partner, we verified that this POC does not work on Docker, nor on Nginx when it is configured as a reverse proxy - and we do not intend to spend time investigating why, as we consider it irrelevant. In any case, this POC works on common production stacks and must be addressed. |
Updated comments for clarity and accuracy regarding HTTP/2 and HTTP/3 behaviors.
Updated reference for HTTP/3 protocol compliance.
Co-authored-by: Max Leske <250711+theseion@users.noreply.github.com>
|
I'm a bit late to the party here. I understand you did not get anywhere here, then you guys took it to Slack, where you understood each other a bit better. I agree this looks dangerous and we need to come up with a fix. Could one of you explain the remaining problem here in new words? I like the reference to |
|
Thank you, Christian, for your feedback. I’ve updated the description with a complete reproduction schema as well as the findings we observed. |
|
This was fixed by #4406. Closing. |
Hello,
This PR follow this one : #4339
OWASP CRS has a few weaknesses regarding content-type evasions that aim to bypass XML and JSON processors - we maintain around few internal rules to enforce stricter handling of these cases.
Here’s the first one, which addresses this specific payload:
curl -v --http2 -H "x-format-output: txt-matched-rules" -H "x-crs-paranoia-level:4" "http://sandbox.coreruleset.org/" -d '{"id_order":"select(sleep(10));"}' -H 'Content-Type:' -H 'Content-Length:'Which is currently wrongly caught by 920180 on HTTP/2 - the rule is indeed not triggered as of now.
To observe this behavior - confirmed on the latest versions of Apache2 and Nginx - you can create a PHP file (script.php) at the root of an Internet-accessible site containing the following:
Then call this script as follows:
Make sure your server properly supports HTTP/2.
We found that certain configurations downgrade this request to HTTP/1.1, which makes the POC ineffective. If your setup behaves this way, you will notice it by looking at the cURL debug output (
curl -v).This POC also does not seem to work on Cloudflare, which fully blocks the request.