diff --git a/httplib.h b/httplib.h index 58c63d04d4..952f78cfc9 100644 --- a/httplib.h +++ b/httplib.h @@ -8314,18 +8314,37 @@ inline bool is_multipart_boundary_chars_valid(const std::string &boundary) { return valid; } +// RFC 7578 ยง5.1.1: escape CR, LF and the double quote so a caller-supplied +// field name or filename cannot break out of the quoted-string or inject extra +// part headers. Mirrors the WHATWG form-data escaping used by browsers. +inline std::string escape_multipart_field(const std::string &s) { + std::string r; + r.reserve(s.size()); + for (auto c : s) { + switch (c) { + case '\r': r += "%0D"; break; + case '\n': r += "%0A"; break; + case '"': r += "%22"; break; + default: r += c; break; + } + } + return r; +} + template inline std::string serialize_multipart_formdata_item_begin(const T &item, const std::string &boundary) { std::string body = "--" + boundary + "\r\n"; - body += "Content-Disposition: form-data; name=\"" + item.name + "\""; + body += "Content-Disposition: form-data; name=\"" + + escape_multipart_field(item.name) + "\""; if (!item.filename.empty()) { - body += "; filename=\"" + item.filename + "\""; + body += "; filename=\"" + escape_multipart_field(item.filename) + "\""; } body += "\r\n"; if (!item.content_type.empty()) { - body += "Content-Type: " + item.content_type + "\r\n"; + body += + "Content-Type: " + escape_multipart_field(item.content_type) + "\r\n"; } body += "\r\n"; diff --git a/test/test.cc b/test/test.cc index 3444e85cab..27b5e53d8d 100644 --- a/test/test.cc +++ b/test/test.cc @@ -12398,6 +12398,30 @@ TEST(MultipartFormDataTest, BadHeader) { EXPECT_EQ(StatusCode::BadRequest_400, res->status); } +TEST(MultipartFormDataTest, EscapesFieldInjection) { + UploadFormDataItems items = { + {"field", "data", "evil\r\nContent-Type: text/x-injected\r\n\r\nSMUGGLED", + "text/plain"}, + {"na\"me", "x", "fi\"le", ""}, + }; + + auto body = detail::serialize_multipart_formdata(items, "BOUNDARY"); + + // CR/LF in a filename must not introduce extra part headers or a body. + EXPECT_EQ(std::string::npos, + body.find("\r\nContent-Type: text/x-injected\r\n")); + EXPECT_NE(std::string::npos, + body.find("filename=\"evil%0D%0AContent-Type: " + "text/x-injected%0D%0A%0D%0ASMUGGLED\"")); + + // A double quote must not break out of the quoted-string. + EXPECT_NE(std::string::npos, body.find("name=\"na%22me\"")); + EXPECT_NE(std::string::npos, body.find("filename=\"fi%22le\"")); + + // Well-formed values are left untouched. + EXPECT_NE(std::string::npos, body.find("Content-Type: text/plain\r\n")); +} + TEST(MultipartFormDataTest, WithPreamble) { Server svr; svr.Post("/post", [&](const Request & /*req*/, Response &res) {