Project

General

Profile

Actions

Feature #3219

closed

reject empty Content-Length header for HTTP/1.x

Added by kenballus 9 months ago. Updated 9 months ago.

Status:
Fixed
Priority:
Normal
Category:
core
Target version:
ASK QUESTIONS IN Forums:
Yes

Description

RFC 9110 specifies that the value of a Content-Length header must match the following ABNF rule:
Content-Length = 1*DIGIT
(i.e. one or more ASCII digits)

When Lighttpd receives an HTTP request with an empty Content-Length header value, it is treated as equivalent to Content-Length: 0. To be compliant with the spec, Lighttpd should reject messages containing empty Content-Length header values, because their semantics are undefined, and they are of potential use in HTTP request smuggling attacks.

Actions #1

Updated by gstrauss 9 months ago

  • Status changed from New to Need Feedback
  • Target version deleted (1.4.xx)

because their semantics are undefined

lighttpd historically ignores empty fields. Historically, the advice has been to be strict in what the server sends, but (optionally) more lenient in how the server handles what is received. If the updated RFC 9110 or subsequent RFCs explicitly forbids the behavior, then I will update lighttpd.

If the latest RFCs do not explicitly forbid the behavior, then I will evaluate and consider updating lighttpd to be more strictly conformant, but this is not a bug in lighttpd, and for compatibility with less-strictly compliant clients, I might not end up making any changes.


Please provide more details how and why you think this behavior might constitute request smuggling. Hint: if lighttpd would process the request the same way whether or not you include an invalid, empty Content-Length request header, then you're not smuggling anything different by sending an empty Content-Length request header, or sending none at all. If lighttpd were to send an invalid Content-Length, then that would be a bug, but lighttpd does not do that.

Please be detailed in what protocol you are using (HTTP/1.0, HTTP/1.1, HTTP/2, etc). With HTTP/1.0 and HTTP/1.1, many methods require Content-Length or Transfer-Encoding: chunked (HTTP/1.1), though some methods such as HEAD and GET are permitted to omit Content-Length. HTTP/2 and later packet framing replaces HTTP/1.1 Transfer-Encoding: chunked and Content-Length is optional.

In short, this issue is very likely to be marked invalid unless you can provide concrete evidence (not your opinion) justifying "Improper parsing of empty Content-Length header".

Actions #2

Updated by kenballus 9 months ago

Please be detailed in what protocol you are using

HTTP/1.1.

for compatibility with less-strictly compliant clients, I might not end up making any changes.

Any client that generates empty CL values would be incompatible with nearly every web server, and is therefore very unlikely to exist. Among the HTTP servers that reject empty CL values are aiohttp, Apache httpd, Boost::Beast, Gunicorn, h2o, Microsoft IIS, Jetty, Nginx, Node.js, Apache Tomcat, Tornado, all of the major CDNs, and nearly all major proxies. By accepting empty CL headers, Lighttpd makes the job of HTTP client programmers more difficult because their requests will be accepted by Lighttpd and basically nothing else.

Please provide more details how and why you think this behavior might constitute request smuggling.

I am aware of an HTTP parser in a widely-used server (which I cannot disclose at this time) that makes the opposite assumption to Lighttpd. (i.e. it interprets the message body of a request with an empty Content-Length header to be all bytes received after the request headers.) I suspect that the programmers of this unnamed server are not the only people to have settled on that interpretation of the empty CL header. I am also aware of two widely-used HTTP proxies that are happy to forward requests with empty CL header values intact, in violation of the RFC.

While I could invent a sitaution in which Lighttpd's interpretation of the empty header plays a part in a request smuggling exploit chain involving these proxies and servers, it would only be an invention. If you're looking for a PoC, I have none. That said, Lighttpd's HTTP/1.1 parser does not accept any other malformed CL values. Lighttpd also does not accept CL values consisting of comma-separated lists of duplicate integers, which the RFC states it MAY. It seems strange and inconsistent to forbid all malformed CL values except one, especially while rejecting what some would argue are valid CL headers.

There are places in which Postel's law applies. I believe that the Content-Length header is not one, due to its use in request smuggling and response splitting. If you disagree with me about this, then our disagreement will not be sorted out in this issue thread, and you should therefore feel free to close it.

Thank you for taking the time to read this thread.

Actions #3

Updated by gstrauss 9 months ago

Thanks. I appreciate the details. While I do not agree with your tenuous logical leaps, I will treat your (intentionally) vague assertions of other servers behaviors as true for the sake of continuing the discussion.

I am also aware of two widely-used HTTP proxies that are happy to forward requests with empty CL header values intact, in violation of the RFC.

That is a bug in those HTTP proxies and could contribute to request smuggling through those HTTP proxies. It is not a bug in lighttpd.

(i.e. it interprets the message body of a request with an empty Content-Length header to be all bytes received after the request headers.)

This does not make sense to me for HTTP/1.1. No such behavior is documented in HTTP/1.1 RFCs. No such behavior is implied in the HTTP/1.1 RFCs. HTTP/1.0 might half-close a TCP connection to indicate end of request body, but if I recall correctly, that is not permitted in HTTP/1.1, and also not widely supported by servers since reading EOF is often an indication of TCP connection closed. In HTTP/1.1, a request body must be indicated with a Content-Length of non-negative integer value, or via Transfer-Encoding: chunked.

Whoever made that choice is overloading Content-Length in what I consider a non-intuitive way. lighttpd does not send an empty Content-Length to backends (e.g. mod_proxy or any CGI-based backend, such as FastCGI), so I do not see how that other server treating Content-Length that way is any concern of lighttpd. If that server were to send an empty Content-Length, then as you pointed out, that would be a violation of the RFC and a bug in that server, not lighttpd.

Any client that generates empty CL values would be incompatible with nearly every web server, and is therefore very unlikely to exist. [...] By accepting empty CL headers, Lighttpd makes the job of HTTP client programmers more difficult because their requests will be accepted by Lighttpd and basically nothing else.

Sorry, since the situation is unlikely to exist, I do not believe your conclusion logically follows. As I said previously, lighttpd ignores empty request headers, and so lighttpd's behavior when receiving an empty Content-Length is the same as if Content-Length was not sent at all.

How does this affect HTTP client programmers? HTTP client programmers should not send malformed requests. (If you are writing HTTP/1.1 conformance tests, then, yes, I believe that there is more than one allowed behavior for empty Content-Length.)

I read this issue as a request for lighttpd to reject empty Content-Length. I will consider the feature request.

I do not believe lighttpd's behavior is incorrect, although lighttpd's behavior in this case is not the behavior that you desire. I have not carefully reviewed RFC 9110 et al, but will try to make some time to do so.

Modifying this behavior in lighttpd is not difficult, but might be a measurable cost on the critical path for every header in every request.

While I could invent a sitaution (sic) in which Lighttpd's interpretation of the empty header plays a part in a request smuggling exploit chain involving these proxies and servers, it would only be an invention.

That would be a bug in those other servers, not lighttpd. You seem to be stuck on the idea that lighttpd accepting and ignoring empty Content-Length is wrong, but only because others do it differently. If it is wrong, please help me find evidence of that in the RFCs. Otherwise, it would be unfortunate if the bug in broken proxies were able to send a request to lighttpd with empty Content-Length, which lighttpd ignored, but is not a bug in lighttpd since the behavior in lighttpd is the same as when Content-Length is not provided in the request.

That said, Lighttpd's HTTP/1.1 parser does not accept any other malformed CL values. Lighttpd also does not accept CL values consisting of comma-separated lists of duplicate integers, which the RFC states it MAY. It seems strange and inconsistent to forbid all malformed CL values except one, especially while rejecting what some would argue are valid CL headers.

It seems strange that you have ignored my explanation that lighttpd historically ignores empty request headers. Simple.

Lighttpd also does not accept CL values consisting of comma-separated lists of duplicate integers, which the RFC states it MAY.

That would indicate duplicated Content-Length sent through broken proxies, and that the request was not rejected or fixed by those proxies, and then combined into a single field before the request was sent to lighttpd. I have chosen not to attempt to fix all broken behavior from broken proxies. While it could be coded in lighttpd, it is extra bytes for a malformed request, and lighttpd is permitted by the RFCs to reject that malformed request. lighttpd does just that.

Actions #4

Updated by gstrauss 9 months ago

  • Status changed from Need Feedback to Wontfix

Reading through RFC 9110, I see that it notes that Allow response header can be empty, and Accept-Encoding request header can be empty. Custom headers could be defined to be empty, too.

In lighttpd's case, lighttpd mod_deflate treats absence of Accept-Encoding as the same as empty Accept-Encoding, and so lighttpd ignoring all empty request headers has the same behavior for mod_deflate use of Accept-Encoding.

It is true that lighttpd will not send an empty Allow response header received from a backend. However, the valid use of empty Allow is limited. lighttpd does not currently support this use.

RFC 9110 also notes that any header field that is a list must not be an empty list. lighttpd does not maintain any list of known headers that require lists, and does not generically parse and validate these headers. Where these list headers are used in lighttpd, lighttpd ignores empty elements.

Your statement about the ABNF of Content-Length is true, that Content-Length = 1*DIGIT requires one or more digits, and so an empty Content-Length is malformed.

For simplicity, lighttpd ignores empty request headers, including empty Content-Length.

To special-case and reject empty Content-Length would require extra code on a very hot critical path. At this moment, I do not plan to change this code. The behavior of lighttpd for a request with empty Content-Length is the same as lighttpd behavior for a request without Content-Length, and so I disagree with your suggestion that this is a bug in lighttpd.

lighttpd could more strictly validate requests, such as checking that request headers that take lists are properly formatted and not empty. While lighttpd could do so, lighttpd currently does not. Similarly, while lighttpd could reject empty Content-Length, at the moment, lighttpd chooses to ignore empty Content-Length.

There are places in which Postel's law applies. I believe that the Content-Length header is not one, due to its use in request smuggling and response splitting. If you disagree with me about this, then our disagreement will not be sorted out in this issue thread, and you should therefore feel free to close it.

While Content-Length is often used in request smuggling and response splitting, and I agree that lighttpd could be stricter here, I have evaluated this issue and concluded that this is not a bug in lighttpd and would not result in request smuggling or response splitting through lighttpd. If request smuggling or response splitting occurred in a proxy in front of lighttpd, then that would be a problem in the proxy in front of lighttpd, and not a problem in lighttpd. Therefore, I do not plan to change lighttpd to be stricter in this way at this time.

I appreciate that you have taken the time to evaluate this behavior in lighttpd and to question it. Thank you.

Actions #5

Updated by gstrauss 9 months ago

Checkpoint, in case this is revisited. This would reject empty Content-Length for HTTP/1.x requests, but this code does not check empty Content-Length for HTTP/2 (or later).

--- a/src/request.c
+++ b/src/request.c
@@ -1197,7 +1197,11 @@ static int http_request_parse_headers(request_st * const restrict r, char * cons

         const int vlen = (int)(end - v);
         /* empty header-fields are not allowed by HTTP-RFC, we just ignore them */
-        if (vlen <= 0) continue; /* ignore header */
+        if (__builtin_expect( (vlen <= 0), 0)) {
+            if (id == HTTP_HEADER_CONTENT_LENGTH)
+                return http_request_header_line_invalid(r, 400, "invalid Content-Length header -> 400");
+            continue; /* ignore header */
+        }

         if (http_header_strict) {
             const char * const x = http_request_check_line_strict(v, vlen);


Aside: RFC 9112 HTTP/1.1 requires that servers reject or close the connection after servicing an HTTP/1.1 request containing both Content-Length and Transfer-Encoding. Again, lighttpd currently ignores request headers with empty field values, so ignores empty Content-Length, but I have added code to my dev branch so that the next release of lighttpd will disable keep-alive on an HTTP/1.1 connection where a request is sent with both valid Content-Length (non-empty) and Transfer-Encoding: chunked.

Actions #6

Updated by gstrauss 9 months ago

  • Tracker changed from Bug to Feature
  • Subject changed from Improper parsing of empty Content-Length header to reject empty Content-Length header for HTTP/1.x
  • Status changed from Wontfix to Patch Pending
  • Target version set to 1.4.72

I am also aware of two widely-used HTTP proxies that are happy to forward requests with empty CL header values intact, in violation of the RFC.

HAProxy forwards malformed empty Content-Length headers, in violation of RFC 9110
https://github.com/haproxy/haproxy/issues/2237

ATS forwards malformed empty Content-Length headers, in violation of RFC 9110
https://github.com/apache/trafficserver/issues/10137


In part due to the above issues in HAProxy and Apache Traffic Server, and in part due to the stricter language in RFC9112 when both Content-Length and Transfer-Encoding are sent in the same HTTP/1.1 request, I've reconsidered my position and will add the patch above to reject empty Content-Length for HTTP/1.x. While it adds ~ 8 instructions, I have marked the branch cold and the cost is in the noise for microbenchmarks on my Linux system with kernel mitigations enabled for Spectre and Meltdown variants. Note: even with this patch, lighttpd is still not pedantically compliant with the part of RFC9112 when both Content-Length and Transfer-Encoding are sent in the same HTTP/1.1 request when Content-Length is empty because with this patch, lighttpd rejects the request with 400 Bad Request as soon as empty Content-Length is parsed, which might be before Transfer-Encoding is parsed in the request headers. Empty Content-Length is already malformed and invalid, but I'll adjust this patch to add more layered defense if an exploit is demonstrated through the bugs in those other proxy servers.

I have renamed this issue to a feature request from "Improper parsing of empty Content-Length header" to "reject empty Content-Length header for HTTP/1.x". lighttpd does properly parse Content-Length. Prior to this patch, for HTTP/1.x, lighttpd chose to ignore the invalid empty header rather than to reject empty Content-Length.

Due to protocol framing for HTTP/2 and HTTP/3, for those protocols, an empty Content-Length has less potential impact with regards to request smuggling and response splitting, and lighttpd will continue to ignore empty Content-Length header if received in an HTTP/2 request (and in the future once HTTP/3 is supported in lighttpd, also ignore empty Content-Length in an HTTP/3 request).

Thank you for your research.

Actions #7

Updated by gstrauss 9 months ago

  • Status changed from Patch Pending to Fixed
Actions #8

Updated by kenballus 9 months ago

Thank you for the thoughtful responses. Your concern for the correctness and performance of lighttpd is refreshing.

Actions

Also available in: Atom