correct a out-of-bounds read error in the HTTP server;

pull/128/head
Bryan Biedenkapp 2 months ago
parent 2b3276f3d0
commit 5e15e26547

@ -94,6 +94,15 @@ namespace restapi
accept();
}
/**
* @brief Gets the TCP port on which the server is listening.
* @returns Bound TCP port, including an OS-assigned port when constructed with port 0.
*/
uint16_t localPort() const
{
return m_acceptor.local_endpoint().port();
}
/**
* @brief Run the servers ASIO IO service loop.
*/

@ -24,6 +24,7 @@
#include <array>
#include <memory>
#include <limits>
#include <utility>
#include <iterator>
@ -76,6 +77,7 @@ namespace restapi
m_lexer(HTTPLexer(false)),
m_continue(false),
m_contResult(HTTPLexer::INDETERMINATE),
m_headerBytes(0U),
m_persistent(persistent),
m_debug(debug)
{
@ -130,28 +132,39 @@ namespace restapi
try
{
if (!m_continue) {
std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + recvLength);
m_request.content = std::string();
std::string contentLength = m_request.headers.find("Content-Length");
if (contentLength != "" && (::strlen(content) != 0)) {
size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10);
m_request.contentLength = length;
m_request.content = std::string(content, length);
}
m_request.headers.add("RemoteHost", m_socket.lowest_layer().remote_endpoint().address().to_string());
uint32_t consumed = m_lexer.consumed();
if (result == HTTPLexer::GOOD && consumed == recvLength &&
((m_request.method == HTTP_POST) || (m_request.method == HTTP_PUT))) {
if (m_debug) {
LogDebug(LOG_REST, "HTTPS Partial Request, recvLength = %u, consumed = %u, result = %u", recvLength, consumed, result);
Utils::dump(1U, "SecureServerConnection::read(), m_buffer", (uint8_t*)m_buffer.data(), recvLength);
m_headerBytes += recvLength;
if (m_headerBytes > MAX_HTTP_HEADER_LENGTH) {
result = HTTPLexer::BAD;
} else {
std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + recvLength);
if (result == HTTPLexer::GOOD) {
size_t length = 0U;
const std::string contentLength = m_request.headers.find("Content-Length");
if (!contentLength.empty() && !parseContentLength(contentLength, length)) {
result = HTTPLexer::BAD;
} else {
m_request.contentLength = length;
m_request.content.clear();
const size_t available = static_cast<size_t>((m_buffer.data() + recvLength) - content);
if (available > length) {
result = HTTPLexer::BAD;
} else {
m_request.content.assign(content, available);
m_request.headers.add("RemoteHost", m_socket.lowest_layer().remote_endpoint().address().to_string());
if (available < length) {
if (m_debug) {
LogDebug(LOG_REST, "HTTPS Partial Request, recvLength = %zu, body = %zu/%zu", recvLength,
available, length);
}
m_contResult = result = HTTPLexer::CONTINUE;
m_continue = true;
}
}
}
}
result = HTTPLexer::INDETERMINATE;
m_continue = true;
}
} else {
if (m_debug) {
@ -159,15 +172,18 @@ namespace restapi
Utils::dump(1U, "SecureServerConnection::read(), m_buffer", (uint8_t*)m_buffer.data(), recvLength);
}
if (m_contResult == HTTPLexer::INDETERMINATE) {
m_request.content = std::string(m_buffer.data(), recvLength);
const size_t received = m_request.content.size();
const size_t remaining = m_request.contentLength - received;
if (recvLength > remaining) {
result = HTTPLexer::BAD;
} else {
m_request.content.append(std::string(m_buffer.data(), recvLength));
}
m_request.content.append(m_buffer.data(), recvLength);
if (m_request.contentLength != 0 && recvLength < m_request.contentLength) {
m_contResult = result = HTTPLexer::CONTINUE;
m_continue = true;
if (m_request.content.size() < m_request.contentLength) {
m_contResult = result = HTTPLexer::CONTINUE;
} else {
result = HTTPLexer::GOOD;
}
}
}
@ -178,6 +194,7 @@ namespace restapi
m_continue = false;
m_contResult = HTTPLexer::INDETERMINATE;
m_headerBytes = 0U;
m_requestHandler.handleRequest(m_request, m_reply);
if (m_debug) {
@ -189,6 +206,7 @@ namespace restapi
else if (result == HTTPLexer::BAD) {
m_continue = false;
m_contResult = HTTPLexer::INDETERMINATE;
m_headerBytes = 0U;
m_reply = HTTPPayload::statusPayload(HTTPPayload::BAD_REQUEST);
write();
}
@ -200,6 +218,7 @@ namespace restapi
::LogError(LOG_REST, "SecureServerConnection::read(), %s %s", e.what(), ec.message().c_str());
m_continue = false;
m_contResult = HTTPLexer::INDETERMINATE;
m_headerBytes = 0U;
m_reply = HTTPPayload::statusPayload(HTTPPayload::INTERNAL_SERVER_ERROR);
write();
@ -211,6 +230,7 @@ namespace restapi
}
m_connectionManager.stop(self);
m_continue = false;
m_headerBytes = 0U;
}
});
}
@ -234,6 +254,7 @@ namespace restapi
m_reply.status = HTTPPayload::OK;
m_reply.content = "";
m_request = HTTPPayload();
m_headerBytes = 0U;
read();
}
else {
@ -257,6 +278,34 @@ namespace restapi
});
}
/**
* @brief Parses the Content-Length header value.
* @param value The string value of the Content-Length header.
* @param length The parsed content length.
* @return True if the content length was successfully parsed and is within the allowed limit, false otherwise.
*/
static bool parseContentLength(const std::string& value, size_t& length)
{
if (value.empty())
return false;
size_t parsed = 0U;
for (char c : value) {
if (c < '0' || c > '9')
return false;
const size_t digit = static_cast<size_t>(c - '0');
if (parsed > (std::numeric_limits<size_t>::max() - digit) / 10U)
return false;
parsed = parsed * 10U + digit;
if (parsed > MAX_HTTP_CONTENT_LENGTH)
return false;
}
length = parsed;
return true;
}
asio::ssl::stream<asio::ip::tcp::socket> m_socket;
ConnectionManagerType& m_connectionManager;
@ -270,6 +319,10 @@ namespace restapi
bool m_continue;
HTTPLexer::ResultType m_contResult;
size_t m_headerBytes;
static constexpr size_t MAX_HTTP_HEADER_LENGTH = 32768U;
static constexpr size_t MAX_HTTP_CONTENT_LENGTH = 1048576U;
bool m_persistent;
bool m_debug;

@ -23,6 +23,7 @@
#include <array>
#include <memory>
#include <limits>
#include <utility>
#include <iterator>
@ -73,6 +74,7 @@ namespace restapi
m_lexer(HTTPLexer(false)),
m_continue(false),
m_contResult(HTTPLexer::INDETERMINATE),
m_headerBytes(0U),
m_persistent(persistent),
m_debug(debug)
{
@ -113,28 +115,40 @@ namespace restapi
try
{
if (!m_continue) {
std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + recvLength);
m_request.content = std::string();
std::string contentLength = m_request.headers.find("Content-Length");
if (contentLength != "" && (::strlen(content) != 0)) {
size_t length = (size_t)::strtoul(contentLength.c_str(), NULL, 10);
m_request.contentLength = length;
m_request.content = std::string(content, length);
}
m_request.headers.add("RemoteHost", m_socket.remote_endpoint().address().to_string());
uint32_t consumed = m_lexer.consumed();
if (result == HTTPLexer::GOOD && consumed == recvLength &&
((m_request.method == HTTP_POST) || (m_request.method == HTTP_PUT))) {
if (m_debug) {
LogDebug(LOG_REST, "HTTP Partial Request, recvLength = %u, consumed = %u, result = %u", recvLength, consumed, result);
Utils::dump(1U, "ServerConnection::read(), m_buffer", (uint8_t*)m_buffer.data(), recvLength);
m_headerBytes += recvLength;
if (m_headerBytes > MAX_HTTP_HEADER_LENGTH) {
result = HTTPLexer::BAD;
} else {
std::tie(result, content) = m_lexer.parse(m_request, m_buffer.data(), m_buffer.data() + recvLength);
if (result == HTTPLexer::GOOD) {
size_t length = 0U;
const std::string contentLength = m_request.headers.find("Content-Length");
if (!contentLength.empty() && !parseContentLength(contentLength, length)) {
result = HTTPLexer::BAD;
} else {
m_request.contentLength = length;
m_request.content.clear();
const size_t available = static_cast<size_t>((m_buffer.data() + recvLength) - content);
if (available > length) {
// HTTP pipelining and bytes beyond the declared body are not supported.
result = HTTPLexer::BAD;
} else {
m_request.content.assign(content, available);
m_request.headers.add("RemoteHost", m_socket.remote_endpoint().address().to_string());
if (available < length) {
if (m_debug) {
LogDebug(LOG_REST, "HTTP Partial Request, recvLength = %zu, body = %zu/%zu", recvLength,
available, length);
}
m_contResult = result = HTTPLexer::CONTINUE;
m_continue = true;
}
}
}
}
m_contResult = result = HTTPLexer::INDETERMINATE;
m_continue = true;
}
} else {
if (m_debug) {
@ -142,15 +156,18 @@ namespace restapi
Utils::dump(1U, "ServerConnection::read(), m_buffer", (uint8_t*)m_buffer.data(), recvLength);
}
if (m_contResult == HTTPLexer::INDETERMINATE) {
m_request.content = std::string(m_buffer.data(), recvLength);
const size_t received = m_request.content.size();
const size_t remaining = m_request.contentLength - received;
if (recvLength > remaining) {
result = HTTPLexer::BAD;
} else {
m_request.content.append(std::string(m_buffer.data(), recvLength));
}
m_request.content.append(m_buffer.data(), recvLength);
if (m_request.contentLength != 0 && recvLength < m_request.contentLength) {
m_contResult = result = HTTPLexer::CONTINUE;
m_continue = true;
if (m_request.content.size() < m_request.contentLength) {
m_contResult = result = HTTPLexer::CONTINUE;
} else {
result = HTTPLexer::GOOD;
}
}
}
@ -161,6 +178,7 @@ namespace restapi
m_continue = false;
m_contResult = HTTPLexer::INDETERMINATE;
m_headerBytes = 0U;
m_requestHandler.handleRequest(m_request, m_reply);
if (m_debug) {
@ -172,6 +190,7 @@ namespace restapi
else if (result == HTTPLexer::BAD) {
m_continue = false;
m_contResult = HTTPLexer::INDETERMINATE;
m_headerBytes = 0U;
m_reply = HTTPPayload::statusPayload(HTTPPayload::BAD_REQUEST);
write();
}
@ -183,6 +202,7 @@ namespace restapi
::LogError(LOG_REST, "ServerConnection::read(), %s %s", e.what(), ec.message().c_str());
m_continue = false;
m_contResult = HTTPLexer::INDETERMINATE;
m_headerBytes = 0U;
m_reply = HTTPPayload::statusPayload(HTTPPayload::INTERNAL_SERVER_ERROR);
write();
@ -195,6 +215,7 @@ namespace restapi
m_connectionManager.stop(self);
m_continue = false;
m_contResult = HTTPLexer::INDETERMINATE;
m_headerBytes = 0U;
}
});
}
@ -218,6 +239,7 @@ namespace restapi
m_reply.status = HTTPPayload::OK;
m_reply.content = "";
m_request = HTTPPayload();
m_headerBytes = 0U;
read();
}
else {
@ -241,6 +263,34 @@ namespace restapi
});
}
/**
* @brief Parses the Content-Length header value.
* @param value The string value of the Content-Length header.
* @param length The parsed content length.
* @return True if the content length was successfully parsed and is within the allowed limit, false otherwise.
*/
static bool parseContentLength(const std::string& value, size_t& length)
{
if (value.empty())
return false;
size_t parsed = 0U;
for (char c : value) {
if (c < '0' || c > '9')
return false;
const size_t digit = static_cast<size_t>(c - '0');
if (parsed > (std::numeric_limits<size_t>::max() - digit) / 10U)
return false;
parsed = parsed * 10U + digit;
if (parsed > MAX_HTTP_CONTENT_LENGTH)
return false;
}
length = parsed;
return true;
}
asio::ip::tcp::socket m_socket;
ConnectionManagerType& m_connectionManager;
@ -254,6 +304,10 @@ namespace restapi
bool m_continue;
HTTPLexer::ResultType m_contResult;
size_t m_headerBytes;
static constexpr size_t MAX_HTTP_HEADER_LENGTH = 32768U;
static constexpr size_t MAX_HTTP_CONTENT_LENGTH = 1048576U;
bool m_persistent;
bool m_debug;

@ -12,12 +12,157 @@
#include "common/restapi/http/HTTPLexer.h"
#include "common/restapi/http/HTTPPayload.h"
#include "common/restapi/http/HTTPServer.h"
#include <asio.hpp>
#include <array>
#include <atomic>
#include <chrono>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <tuple>
#include <vector>
using namespace restapi::http;
// ---------------------------------------------------------------------------
// Global Functions
// ---------------------------------------------------------------------------
namespace {
// ---------------------------------------------------------------------------
// Structure Declaration
// ---------------------------------------------------------------------------
/**
* @brief Represents the state of an HTTP request for testing purposes.
*/
struct RequestState {
std::atomic<unsigned int> calls { 0U };
std::mutex mutex;
HTTPPayload request;
};
/**
* @brief Sends a raw HTTP request to the specified port using the provided fragments and returns the response.
* @param port The port to which the HTTP request should be sent.
* @param fragments The fragments of the HTTP request to be sent.
* @return The raw HTTP response received from the server.
*/
std::string sendRawRequest(uint16_t port, const std::vector<std::string>& fragments)
{
asio::io_service ioService;
asio::ip::tcp::socket socket(ioService);
socket.connect(asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), port));
for (const std::string& fragment : fragments) {
asio::write(socket, asio::buffer(fragment));
if (fragments.size() > 1U)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
std::string response;
std::array<char, 4096U> buffer {};
asio::error_code ec;
do {
const size_t length = socket.read_some(asio::buffer(buffer), ec);
response.append(buffer.data(), length);
} while (!ec);
REQUIRE(ec == asio::error::eof);
return response;
}
std::string responseBody(const std::string& response)
{
const size_t separator = response.find("\r\n\r\n");
REQUIRE(separator != std::string::npos);
return response.substr(separator + 4U);
}
}
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief A request handler that echoes back the received HTTP request content.
*/
class EchoRequestHandler {
public:
/**
* @brief Initializes a new instance of the EchoRequestHandler.
*/
EchoRequestHandler() : m_state(std::make_shared<RequestState>()) { }
/**
* @brief Initializes a new instance of the EchoRequestHandler with the specified request state.
*/
explicit EchoRequestHandler(std::shared_ptr<RequestState> state) : m_state(std::move(state)) { }
/**
* @brief Handles an incoming HTTP request and prepares the corresponding reply.
* @param request The incoming HTTP request.
* @param reply The HTTP reply to be sent back to the client.
*/
void handleRequest(const HTTPPayload& request, HTTPPayload& reply)
{
// scope is intentional
{
std::lock_guard<std::mutex> lock(m_state->mutex);
m_state->request = request;
}
++m_state->calls;
std::string content = request.content;
reply.payload(content, HTTPPayload::OK, "application/octet-stream");
}
private:
std::shared_ptr<RequestState> m_state;
};
// ---------------------------------------------------------------------------
// Class Declaration
// ---------------------------------------------------------------------------
/**
* @brief A simple loopback HTTP server for testing purposes.
*/
class LoopbackHTTPServer {
public:
/**
* @brief Initializes an instance of the loopback HTTP server.
*/
LoopbackHTTPServer() :
state(std::make_shared<RequestState>()),
server("127.0.0.1", 0U, false)
{
server.setHandler(EchoRequestHandler(state));
server.open();
port = server.localPort();
thread = std::thread([this]() { server.run(); });
}
/**
* @brief Finalizes and stops the loopback HTTP server.
*/
~LoopbackHTTPServer()
{
server.stop();
if (thread.joinable())
thread.join();
}
LoopbackHTTPServer(const LoopbackHTTPServer&) = delete;
LoopbackHTTPServer& operator=(const LoopbackHTTPServer&) = delete;
std::shared_ptr<RequestState> state;
HTTPServer<EchoRequestHandler> server;
uint16_t port { 0U };
std::thread thread;
};
TEST_CASE("HTTPLexer parses a valid HTTP request", "[restapi][http][lexer]")
{
const std::string request =
@ -100,3 +245,76 @@ TEST_CASE("HTTP status payload applies default REST headers", "[restapi][http][p
REQUIRE(payload.headers.find("Content-Length") == std::to_string(payload.content.size()));
REQUIRE(payload.headers.find("Server").empty() == false);
}
TEST_CASE("HTTP server handles a complete request over TCP", "[restapi][http][e2e]")
{
LoopbackHTTPServer fixture;
const std::string body("echo\0payload", 12U);
const std::string request =
"PUT /echo HTTP/1.1\r\n"
"Host: localhost\r\n"
"Content-Length: " + std::to_string(body.size()) + "\r\n"
"\r\n" + body;
const std::string response = sendRawRequest(fixture.port, { request });
REQUIRE(response.find("HTTP/1.0 200 OK\r\n") == 0U);
REQUIRE(responseBody(response) == body);
REQUIRE(fixture.state->calls == 1U);
std::lock_guard<std::mutex> lock(fixture.state->mutex);
REQUIRE(fixture.state->request.method == "PUT");
REQUIRE(fixture.state->request.uri == "/echo");
REQUIRE(fixture.state->request.content == body);
}
TEST_CASE("HTTP server accumulates a fragmented request body", "[restapi][http][e2e]")
{
LoopbackHTTPServer fixture;
const std::vector<std::string> fragments {
"PUT /fragmented HTTP/1.1\r\nHost: localhost\r\nContent-Length: 11\r\n\r\n",
"hello ",
"world"
};
const std::string response = sendRawRequest(fixture.port, fragments);
REQUIRE(response.find("HTTP/1.0 200 OK\r\n") == 0U);
REQUIRE(responseBody(response) == "hello world");
REQUIRE(fixture.state->calls == 1U);
}
TEST_CASE("HTTP server rejects malformed Content-Length", "[restapi][http][e2e][negative]")
{
LoopbackHTTPServer fixture;
const std::string request =
"PUT /echo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 12x\r\n\r\n";
const std::string response = sendRawRequest(fixture.port, { request });
REQUIRE(response.find("HTTP/1.0 400 Bad Request\r\n") == 0U);
REQUIRE(fixture.state->calls == 0U);
}
TEST_CASE("HTTP server rejects an oversized declared body", "[restapi][http][e2e][negative]")
{
LoopbackHTTPServer fixture;
const std::string request =
"PUT /echo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 1048577\r\n\r\n";
const std::string response = sendRawRequest(fixture.port, { request });
REQUIRE(response.find("HTTP/1.0 400 Bad Request\r\n") == 0U);
REQUIRE(fixture.state->calls == 0U);
}
TEST_CASE("HTTP server rejects bytes beyond the declared body", "[restapi][http][e2e][negative]")
{
LoopbackHTTPServer fixture;
const std::string request =
"PUT /echo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 4\r\n\r\nextra";
const std::string response = sendRawRequest(fixture.port, { request });
REQUIRE(response.find("HTTP/1.0 400 Bad Request\r\n") == 0U);
REQUIRE(fixture.state->calls == 0U);
}

Loading…
Cancel
Save

Powered by TurnKey Linux.