]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Reject different HTTP requests with unusual framing (#753)
authorAmos Jeffries <yadij@users.noreply.github.com>
Tue, 20 Jul 2021 19:01:40 +0000 (19:01 +0000)
committerSquid Anubis <squid-anubis@squid-cache.org>
Tue, 20 Jul 2021 21:37:16 +0000 (21:37 +0000)
... and remove support for request_entities.

Squid now follows the following (approximate) rules when checking HTTP
request framing. The first matching rule wins.

* HTTP requests with a Transfer-Encoding:chunked header, including GET
  and HEAD requests with that header, are accepted. No changes here.

* HTTP requests with unsupported Transfer-Encoding values are rejected
  (Squid replies with HTTP 501 "Not Implemented"). No changes here.

* HTTP requests having conflicting Content-Length values are rejected
  (Squid replies with HTTP 400 "Bad Request"). No changes here.

* HTTP/1.0 and HTTP/0.9 POST and PUT requests without a valid
  Content-Length header are now rejected (Squid replies with HTTP 411
  "Length Required"). All of these were allowed before.

* HTTP/1.0 GET and HEAD requests with a Content-Length:0 header are now
  rejected (Squid replies with HTTP 400 "Bad Request"). All of these
  were allowed before.

* HTTP/1.0 GET and HEAD requests with a positive Content-Length header
  are now rejected (Squid replies with HTTP 400 "Bad Request"). All of
  these were allowed before if and only if the request_entities
  directive was explicitly set to "on".

There are no other framing-related HTTP request restrictions. Prior to
these changes, HTTP/1.1 GET and HEAD requests with a positive
Content-Length header were rejected unless the request_entities
directive was explicitly set to "on". The following configuration sketch
keeps rejecting those requests:

    acl getOrHead method GET HEAD
    acl withContentLength req_header Content-Length .
    http_access deny getOrHead withContentLength

The new restrictions were added due to possibility of cache corruption
attacks and other security issues related to HTTP request framing.

The request_entities directive was removed to simplify decision logic.

Some developers believe that these changes should be accompanied by
configuration options that allow admins to bypass (most of) the
previously absent restrictions. However, these developers do not know of
any important use cases that these changes break, and such cases may not
even exist. The authors insist on these security-driven changes.

doc/release-notes/release-6.sgml
src/HttpRequest.cc
src/HttpRequest.h
src/SquidConfig.h
src/cf.data.pre
src/client_side.cc
src/http/MethodType.h

index d31ee8b4e7674dd9e1f25c8836e974b6c8e3c2cc..3efeba84a7c35d621d699427e00aba612656534e 100644 (file)
@@ -69,8 +69,23 @@ This section gives an account of those changes in three categories:
 <sect1>Removed directives<label id="removeddirectives">
 <p>
 <descrip>
-       <p>There have been no directives changed.
-
+       <tag>request_entities</tag>
+       <p>Obsolete. Squid accepts an entity (aka payload, body) on
+          HTTP/1.1 GET or HEAD requests when a Content-Length or
+          Transfer-Encoding header is presented to clearly determine size.
+       <p>To retain the old behaviour of rejecting GET/HEAD payloads
+          for HTTP/1.1 use <em>http_access</em> rules:
+<verb>
+  acl fetch method GET HEAD
+  acl entity req_header Content-Length .
+  http_access deny fetch entity
+</verb>
+       <p>Squid will reject use of Content-Length header on HTTP/1.0
+          messages with GET, HEAD, DELETE, LINK, UNLINK methods. Since
+          the HTTP/1.0 specification defines those as not having entities.
+          To deliver entities on these methods the chunked encoding
+          feature defined by HTTP/1.1 must be used, or the request
+          upgraded to an HTTP/1.1 message.
 </descrip>
 
 
index 6dcce96f7c8bdd0f351e538882932fc0e46ad1f5..4e0c2e8e514080908ed1e1a705ecf89788f93556 100644 (file)
@@ -651,6 +651,70 @@ HttpRequest::canHandle1xx() const
     return true;
 }
 
+Http::StatusCode
+HttpRequest::checkEntityFraming() const
+{
+    // RFC 7230 section 3.3.1:
+    // "
+    //  A server that receives a request message with a transfer coding it
+    //  does not understand SHOULD respond with 501 (Not Implemented).
+    // "
+    if (header.unsupportedTe())
+        return Http::scNotImplemented;
+
+    // RFC 7230 section 3.3.3 #3 paragraph 3:
+    // Transfer-Encoding overrides Content-Length
+    if (header.chunked())
+        return Http::scNone;
+
+    // RFC 7230 Section 3.3.3 #4:
+    // conflicting Content-Length(s) mean a message framing error
+    if (header.conflictingContentLength())
+        return Http::scBadRequest;
+
+    // HTTP/1.0 requirements differ from HTTP/1.1
+    if (http_ver <= Http::ProtocolVersion(1,0)) {
+        const auto m = method.id();
+
+        // RFC 1945 section 8.3:
+        // "
+        //   A valid Content-Length is required on all HTTP/1.0 POST requests.
+        // "
+        // RFC 1945 Appendix D.1.1:
+        // "
+        //   The fundamental difference between the POST and PUT requests is
+        //   reflected in the different meaning of the Request-URI.
+        // "
+        if (m == Http::METHOD_POST || m == Http::METHOD_PUT)
+            return (content_length >= 0 ? Http::scNone : Http::scLengthRequired);
+
+        // RFC 1945 section 7.2:
+        // "
+        //   An entity body is included with a request message only when the
+        //   request method calls for one.
+        // "
+        // section 8.1-2: GET and HEAD do not define ('call for') an entity
+        if (m == Http::METHOD_GET || m == Http::METHOD_HEAD)
+            return (content_length < 0 ? Http::scNone : Http::scBadRequest);
+        // appendix D1.1.2-4: DELETE, LINK, UNLINK do not define ('call for') an entity
+        if (m == Http::METHOD_DELETE || m == Http::METHOD_LINK || m == Http::METHOD_UNLINK)
+            return (content_length < 0 ? Http::scNone : Http::scBadRequest);
+
+        // other methods are not defined in RFC 1945
+        // assume they support an (optional) entity
+        return Http::scNone;
+    }
+
+    // RFC 7230 section 3.3
+    // "
+    //   The presence of a message body in a request is signaled by a
+    //   Content-Length or Transfer-Encoding header field.  Request message
+    //   framing is independent of method semantics, even if the method does
+    //   not define any use for a message body.
+    // "
+    return Http::scNone;
+}
+
 bool
 HttpRequest::parseHeader(Http1::Parser &hp)
 {
index 7a319f226db832e7a74ef0b92de7c363d9f63bd5..820f4f01de13a3cbbca076cbc6761f0af8cb0bec 100644 (file)
@@ -248,6 +248,10 @@ public:
 
     virtual void configureContentLengthInterpreter(Http::ContentLengthInterpreter &) {}
 
+    /// Check whether the message framing headers are valid.
+    /// \returns Http::scNone or an HTTP error status
+    Http::StatusCode checkEntityFraming() const;
+
     /// Parses request header using Parser.
     /// Use it in contexts where the Parser object is available.
     bool parseHeader(Http1::Parser &hp);
index 0117ea78083f4d7f16a7eec2e0187b0bcf5f8c11..f9c2ff39f9326039d3797abb9c67c7f75fca7751 100644 (file)
@@ -318,7 +318,6 @@ public:
 
         int vary_ignore_expire;
         int surrogate_is_remote;
-        int request_entities;
         int detect_broken_server_pconns;
         int relaxed_header_parser;
         int check_hostnames;
index a5d0624b7ef5f2d81866d713af5c2aeaecb0822b..be6741ec2efbcd83e13ee68924c938b51b024d09 100644 (file)
@@ -181,6 +181,14 @@ DOC_START
        This option is not yet supported by Squid-3.
 DOC_END
 
+# Options removed in 6.x
+NAME: request_entities
+TYPE: obsolete
+DOC_START
+       Remove this line. Squid now accepts HTTP/1.1 requests with bodies.
+       To simplify UI and code, Squid rejects certain HTTP/1.0 requests with bodies.
+DOC_END
+
 # Options removed in 5.x
 NAME: dns_v4_first
 TYPE: obsolete
@@ -6758,22 +6766,6 @@ DOC_START
        varying objects not intended for caching to get cached.
 DOC_END
 
-NAME: request_entities
-TYPE: onoff
-LOC: Config.onoff.request_entities
-DEFAULT: off
-DOC_START
-       Squid defaults to deny GET and HEAD requests with request entities,
-       as the meaning of such requests are undefined in the HTTP standard
-       even if not explicitly forbidden.
-
-       Set this directive to on if you have clients which insists
-       on sending request entities in GET or HEAD requests. But be warned
-       that there is server software (both proxies and web servers) which
-       can fail to properly process this kind of request which may make you
-       vulnerable to cache pollution attacks if enabled.
-DOC_END
-
 NAME: request_header_access
 IFDEF: USE_HTTP_VIOLATIONS
 TYPE: http_header_access
index 004f9c907d37cfa1fad0d4343d6a34f9bdafa1d8..8e1659a028fd28cc3a2b610babe3da785d35854a 100644 (file)
@@ -180,7 +180,6 @@ static IOACB httpAccept;
 #if USE_IDENT
 static IDCB clientIdentDone;
 #endif
-static int clientIsContentLengthValid(HttpRequest * r);
 static int clientIsRequestBodyTooLargeForPolicy(int64_t bodyLength);
 
 static void clientUpdateStatHistCounters(const LogTags &logType, int svc_time);
@@ -698,31 +697,6 @@ clientSetKeepaliveFlag(ClientHttpRequest * http)
     request->flags.proxyKeepalive = request->persistent();
 }
 
-/// checks body length of non-chunked requests
-static int
-clientIsContentLengthValid(HttpRequest * r)
-{
-    // No Content-Length means this request just has no body, but conflicting
-    // Content-Lengths mean a message framing error (RFC 7230 Section 3.3.3 #4).
-    if (r->header.conflictingContentLength())
-        return 0;
-
-    switch (r->method.id()) {
-
-    case Http::METHOD_GET:
-
-    case Http::METHOD_HEAD:
-        /* We do not want to see a request entity on GET/HEAD requests */
-        return (r->content_length <= 0 || Config.onoff.request_entities);
-
-    default:
-        /* For other types of requests we don't care */
-        return 1;
-    }
-
-    /* NOT REACHED */
-}
-
 int
 clientIsRequestBodyTooLargeForPolicy(int64_t bodyLength)
 {
@@ -1697,11 +1671,9 @@ clientProcessRequest(ConnStateData *conn, const Http1::RequestParserPointer &hp,
         request->http_ver.minor = http_ver.minor;
     }
 
-    const auto unsupportedTe = request->header.unsupportedTe();
-
     mustReplyToOptions = (request->method == Http::METHOD_OPTIONS) &&
                          (request->header.getInt64(Http::HdrType::MAX_FORWARDS) == 0);
-    if (!urlCheckRequest(request.getRaw()) || mustReplyToOptions || unsupportedTe) {
+    if (!urlCheckRequest(request.getRaw()) || mustReplyToOptions) {
         clientStreamNode *node = context->getClientReplyContext();
         conn->quitAfterError(request.getRaw());
         clientReplyContext *repContext = dynamic_cast<clientReplyContext *>(node->data.getRaw());
@@ -1714,15 +1686,13 @@ clientProcessRequest(ConnStateData *conn, const Http1::RequestParserPointer &hp,
         return;
     }
 
-    const auto chunked = request->header.chunked();
-    if (!chunked && !clientIsContentLengthValid(request.getRaw())) {
+    const auto frameStatus = request->checkEntityFraming();
+    if (frameStatus != Http::scNone) {
         clientStreamNode *node = context->getClientReplyContext();
         clientReplyContext *repContext = dynamic_cast<clientReplyContext *>(node->data.getRaw());
         assert (repContext);
         conn->quitAfterError(request.getRaw());
-        repContext->setReplyToError(ERR_INVALID_REQ,
-                                    Http::scLengthRequired, request->method, NULL,
-                                    conn, request.getRaw(), nullptr, nullptr);
+        repContext->setReplyToError(ERR_INVALID_REQ, frameStatus, request->method, nullptr, conn, request.getRaw(), nullptr, nullptr);
         assert(context->http->out.offset == 0);
         context->pullData();
         clientProcessRequestFinished(conn, request);
@@ -1744,6 +1714,7 @@ clientProcessRequest(ConnStateData *conn, const Http1::RequestParserPointer &hp,
 #endif
 
     /* Do we expect a request-body? */
+    const auto chunked = request->header.chunked();
     expectBody = chunked || request->content_length > 0;
     if (!context->mayUseConnection() && expectBody) {
         request->body_pipe = conn->expectRequestBody(
index 41fa7c25e28c95021b5e81945ed3cbcc152e2102..7842ce156c2503fa71adf75eb7a1e921d534f41a 100644 (file)
@@ -31,11 +31,9 @@ typedef enum _method_t {
     METHOD_OPTIONS,
     METHOD_DELETE,
 
-#if NO_SPECIAL_HANDLING
     // RFC 2068
     METHOD_LINK,
     METHOD_UNLINK,
-#endif
 
     // RFC 3253
     METHOD_CHECKOUT,