Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -7504,6 +7504,13 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,

inline ssize_t write_request_line(Stream &strm, const std::string &method,
const std::string &path) {
// A request target must not carry CR/LF (or other control octets); otherwise
// a value smuggled into it splits the request line and injects headers or a
// whole request. The same field-value check already guards header values in
// check_and_write_headers and the request target in
// perform_websocket_handshake; apply it here too.
if (!fields::is_field_value(path)) { return -1; }

std::string s = method;
s += ' ';
s += path;
Expand Down
38 changes: 38 additions & 0 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7896,6 +7896,44 @@ TEST(ServerResponseSplittingTest, ChunkedTrailerCRLFInjection) {
EXPECT_NE(std::string::npos, res.find("X-Safe: safe"));
}

// Forward declaration: in split builds split.py strips `inline` and moves the
// definition into httplib.cc, so detail::write_request_line is not visible from
// the public httplib.h. Re-declaring it here lets the tests link against the
// symbol in both header-only and split builds.
namespace httplib {
namespace detail {
ssize_t write_request_line(Stream &strm, const std::string &method,
const std::string &path);
} // namespace detail
} // namespace httplib

TEST(RequestLineInjectionTest, RejectsCRLFInTarget) {
// A well-formed target is written verbatim.
{
detail::BufferStream strm;
auto n = detail::write_request_line(strm, "GET", "/path?a=b");
EXPECT_GT(n, 0);
EXPECT_EQ("GET /path?a=b HTTP/1.1\r\n", strm.get_buffer());
}

// A target carrying CR/LF must be rejected before anything reaches the wire,
// otherwise it splits the request line and injects a header or a whole
// request. This is what a decoded redirect Location ("%0D%0A") turns into
// when path encoding is disabled.
const std::string evil_targets[] = {
"/a\r\nInjected: pwned",
"/a\rInjected",
"/a\nInjected",
"/a\r\n\r\nGET /evil HTTP/1.1\r\nHost: victim\r\n\r\n",
};
for (const auto &evil : evil_targets) {
detail::BufferStream strm;
auto n = detail::write_request_line(strm, "GET", evil);
EXPECT_LT(n, 0);
EXPECT_TRUE(strm.get_buffer().empty());
}
}

// Sends a raw request and verifies that there isn't a crash or exception.
static void test_raw_request(const std::string &req,
std::string *out = nullptr) {
Expand Down
Loading