diff --git a/CHANGELOG.md b/CHANGELOG.md index 0578bc56..c94c8a00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,20 @@ Version 70: * Serialize in one step when possible +* Add basic_parser header and body limits API Changes: * Rename to message::base +* basic_parser default limits are now 1MB/8MB Actions Required: * Change calls to message::header_part() with message::base() +* Call body_limit and/or header_limit as needed to adjust the + limits to suitable values if the defaults are insufficient. + -------------------------------------------------------------------------------- Version 69: diff --git a/doc/5_07_parser_buffers.qbk b/doc/5_07_parser_buffers.qbk index a3455394..11be8576 100644 --- a/doc/5_07_parser_buffers.qbk +++ b/doc/5_07_parser_buffers.qbk @@ -23,7 +23,7 @@ if the underlying connection is closed), [heading Parser Options] -The parser provides two options which may be set before parsing begins: +The parser provides a few options which may be set before parsing begins: [table Parser Options [[Name][Default][Description]] @@ -58,6 +58,27 @@ The parser provides two options which may be set before parsing begins: no body is expected. The parser will consider the message complete after the header has been received. ]] +[[ + [link beast.ref.beast__http__basic_parser.body_limit `body_limit`] +][ + 1MB/8MB +][ + This function sets the maximum allowed size of the content body. + When a body larger than the specified size is detected, an error + is generated and parsing terminates. This setting helps protect + servers from resource exhaustion attacks. The default limit when + parsing requests is 1MB, and for parsing responses 8MB. +]] +[[ + [link beast.ref.beast__http__basic_parser.header_limit `header_limit`] +][ + 8KB +][ + This function sets the maximum allowed size of the header + including all field name, value, and delimiter characters + and also including the CRLF sequences in the serialized + input. +]] ] diff --git a/include/beast/http/basic_parser.hpp b/include/beast/http/basic_parser.hpp index 672de3c1..efe9ef6c 100644 --- a/include/beast/http/basic_parser.hpp +++ b/include/beast/http/basic_parser.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -105,30 +106,50 @@ class basic_parser static unsigned constexpr flagUpgrade = 1<< 12; static unsigned constexpr flagFinalChunk = 1<< 13; - std::uint64_t len_; // size of chunk or body - std::unique_ptr buf_; - std::size_t buf_len_ = 0; - std::size_t skip_ = 0; // search from here - state state_ = state::nothing_yet; - unsigned f_ = 0; // flags + static + std::uint64_t + default_body_limit(std::true_type) + { + // limit for requests + return 1 * 1024 * 1024; // 1MB + } + + static + std::uint64_t + default_body_limit(std::false_type) + { + // limit for responses + return 8 * 1024 * 1024; // 8MB + } + + std::uint64_t body_limit_; // max payload body + std::uint64_t len_; // size of chunk or body + std::unique_ptr buf_; // temp storage + std::size_t buf_len_ = 0; // size of buf_ + std::size_t skip_ = 0; // resume search here + std::uint32_t + header_limit_ = 8192; // max header size + state state_ = // initial state + state::nothing_yet; + unsigned f_ = 0; // flags public: + /// `true` if this parser parses requests, `false` for responses. + using is_request = + std::integral_constant; + /// Copy constructor (disallowed) basic_parser(basic_parser const&) = delete; /// Copy assignment (disallowed) basic_parser& operator=(basic_parser const&) = delete; - /// Default constructor - basic_parser() = default; - - /// `true` if this parser parses requests, `false` for responses. - using is_request = - std::integral_constant; - /// Destructor ~basic_parser() = default; + /// Default constructor + basic_parser();; + /** Move constructor After the move, the only valid operation on the @@ -246,6 +267,58 @@ public: return (f_ & flagNeedEOF) != 0; } + /** Set the limit on the payload body. + + This function sets the maximum allowed size of the payload body, + before any encodings except chunked have been removed. Depending + on the message semantics, one of these cases will apply: + + @li The Content-Length is specified and exceeds the limit. In + this case the result @ref error::body_limit is returned + immediately after the header is parsed. + + @li The Content-Length is unspecified and the chunked encoding + is not specified as the last encoding. In this case the end of + message is determined by the end of file indicator on the + associated stream or input source. If a sufficient number of + body payload octets are presented to the parser to exceed the + configured limit, the parse fails with the result + @ref error::body_limit + + @li The Transfer-Encoding specifies the chunked encoding as the + last encoding. In this case, when the number of payload body + octets produced by removing the chunked encoding exceeds + the configured limit, the parse fails with the result + @ref error::body_limit. + + Setting the limit after any body octets have been parsed + results in undefined behavior. + + The default limit is 1MB for requests and 8MB for responses. + + @param v The payload body limit to set + */ + void + body_limit(std::uint64_t v) + { + body_limit_ = v; + } + + /** Set a limit on the total size of the header. + + This function sets the maximum allowed size of the header + including all field name, value, and delimiter characters + and also including the CRLF sequences in the serialized + input. If the end of the header is not found within the + limit of the header size, the error @ref error::header_limit + is returned by @ref put. + */ + void + header_limit(std::uint32_t v) + { + header_limit_ = v; + } + /// Returns `true` if the eager parse option is set. bool eager() const diff --git a/include/beast/http/error.hpp b/include/beast/http/error.hpp index fe45e690..861620e3 100644 --- a/include/beast/http/error.hpp +++ b/include/beast/http/error.hpp @@ -80,6 +80,20 @@ enum class error */ buffer_overflow, + /** Header limit exceeded. + + The parser detected an incoming message header which + exceeded a configured limit. + */ + header_limit, + + /** Body limit exceeded. + + The parser detected an incoming message body which + exceeded a configured limit. + */ + body_limit, + /** A memory allocation failed. When basic_fields throws std::bad_alloc, it is diff --git a/include/beast/http/impl/basic_parser.ipp b/include/beast/http/impl/basic_parser.ipp index b9df7cb0..3b93cb2f 100644 --- a/include/beast/http/impl/basic_parser.ipp +++ b/include/beast/http/impl/basic_parser.ipp @@ -55,12 +55,21 @@ skip_ows_rev2( } // detail +template +basic_parser:: +basic_parser() + : body_limit_( + default_body_limit(is_request{})) +{ +} + template template basic_parser:: basic_parser(basic_parser< isRequest, OtherDerived>&& other) - : len_(other.len_) + : body_limit_(other.body_limit_) + , len_(other.len_) , buf_(std::move(other.buf_)) , buf_len_(other.buf_len_) , skip_(other.skip_) @@ -294,9 +303,11 @@ basic_parser:: parse_header(char const*& p, std::size_t n, error_code& ec) { + if( n > header_limit_) + n = header_limit_; if(n < skip_ + 4) { - ec = http::error::need_more; + ec = error::need_more; return; } auto const term = @@ -304,6 +315,11 @@ parse_header(char const*& p, if(! term) { skip_ = n - 3; + if(skip_ + 4 > header_limit_) + { + ec = error::header_limit; + return; + } ec = http::error::need_more; return; } @@ -528,6 +544,12 @@ basic_parser:: parse_body_to_eof(char const*& p, std::size_t n, error_code& ec) { + if(n > body_limit_) + { + ec = error::body_limit; + return; + } + body_limit_ = body_limit_ - n; impl().on_data(string_view{p, n}, ec); if(ec) return; @@ -596,6 +618,12 @@ parse_chunk_header(char const*& p0, } if(v != 0) { + if(v > body_limit_) + { + ec = error::body_limit; + return; + } + body_limit_ -= v; if(*p == ';') { // VFALCO TODO Validate extension @@ -868,6 +896,12 @@ do_field(field f, return; } + if(v > body_limit_) + { + ec = error::body_limit; + return; + } + ec.assign(0, ec.category()); len_ = v; f_ |= flagContentLength; diff --git a/include/beast/http/impl/error.ipp b/include/beast/http/impl/error.ipp index 2148674c..fc243851 100644 --- a/include/beast/http/impl/error.ipp +++ b/include/beast/http/impl/error.ipp @@ -44,6 +44,8 @@ public: case error::unexpected_body: return "unexpected body"; case error::need_buffer: return "need buffer"; case error::buffer_overflow: return "buffer overflow"; + case error::header_limit: return "header limit exceeded"; + case error::body_limit: return "body limit exceeded"; case error::bad_alloc: return "bad alloc"; case error::bad_line_ending: return "bad line ending"; case error::bad_method: return "bad method"; diff --git a/test/http/basic_parser.cpp b/test/http/basic_parser.cpp index 84831c6e..b3ed989f 100644 --- a/test/http/basic_parser.cpp +++ b/test/http/basic_parser.cpp @@ -615,7 +615,6 @@ public: length(c("1\r\n"), 1); length(c("01\r\n"), 1); length(c("9\r\n"), 9); - length(c("123456789\r\n"), 123456789); length(c("42 \r\n"), 42); length(c("42\t\r\n"), 42); length(c("42 \t \r\n"), 42); @@ -923,6 +922,68 @@ public: BEAST_EXPECT(p.body == "*****"); } + void + testLimits() + { + { + multi_buffer b; + ostream(b) << + "POST / HTTP/1.1\r\n" + "Content-Length: 2\r\n" + "\r\n" + "**"; + error_code ec; + test_parser p; + p.header_limit(10); + p.eager(true); + p.put(b.data(), ec); + BEAST_EXPECTS(ec == error::header_limit, ec.message()); + } + { + multi_buffer b; + ostream(b) << + "POST / HTTP/1.1\r\n" + "Content-Length: 2\r\n" + "\r\n" + "**"; + error_code ec; + test_parser p; + p.body_limit(1); + p.eager(true); + p.put(b.data(), ec); + BEAST_EXPECTS(ec == error::body_limit, ec.message()); + } + { + multi_buffer b; + ostream(b) << + "HTTP/1.1 200 OK\r\n" + "\r\n" + "**"; + error_code ec; + test_parser p; + p.body_limit(1); + p.eager(true); + p.put(b.data(), ec); + BEAST_EXPECTS(ec == error::body_limit, ec.message()); + } + { + multi_buffer b; + ostream(b) << + "POST / HTTP/1.1\r\n" + "Transfer-Encoding: chunked\r\n" + "\r\n" + "2\r\n" + "**\r\n" + "0\r\n\r\n"; + error_code ec; + test_parser p; + p.body_limit(1); + p.eager(true); + p.put(b.data(), ec); + BEAST_EXPECTS(ec == error::body_limit, ec.message()); + } + } + //-------------------------------------------------------------------------- template @@ -1088,6 +1149,7 @@ public: testUpgradeField(); testBody(); testSplit(); + testLimits(); testIssue430(); testIssue452(); testIssue496(); diff --git a/test/http/error.cpp b/test/http/error.cpp index e5604a0a..52d986bb 100644 --- a/test/http/error.cpp +++ b/test/http/error.cpp @@ -42,6 +42,7 @@ public: check("beast.http", error::unexpected_body); check("beast.http", error::need_buffer); check("beast.http", error::buffer_overflow); + check("beast.http", error::body_limit); check("beast.http", error::bad_alloc); check("beast.http", error::bad_line_ending);