Browse Source

Add Server::set_compress() to turn off handler response compression

A server built with compression support compressed every handler
response the client accepted, with no way to turn it off. Over HTTPS
the length of a compressed response can give away a secret in it when
the same response also carries text an attacker chooses (BREACH).
set_compress(false) sends handler responses, buffered or streamed, as
they are; compression stays on by default, and static file compression
keeps its own setting. The README and the cookbook describe the switch
and when to use it.
yhirose 1 day ago
parent
commit
fc37d3eae1

+ 2 - 0
README.md

@@ -1590,6 +1590,8 @@ This holds for every kind of response, including the file-backed ones below.
 
 `Vary: Accept-Encoding` is added only to responses the server encoded itself. A handler that chooses between an encoded and an identity representation by reading `Accept-Encoding` should set the field itself, so that shared caches keep the two apart.
 
+Turn this compression off with `svr.set_compress(false)`. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). A server whose responses mix the two should turn compression off, or keep such secrets out of those responses. Static file compression, below, is set separately.
+
 ### Static file compression
 
 Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with:

+ 8 - 0
docs-src/pages/en/cookbook/s08-compress-response.md

@@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
 });
 ```
 
+## Turning it off
+
+```cpp
+svr.set_compress(false);
+```
+
+Handler responses, buffered or streamed, are then sent as they are. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). Static file compression is set separately.
+
 ## Static files need to be opted in
 
 Files served as they are, through `set_mount_point()` or `Response::set_file_content()`, are not compressed by default. Turn it on with:

+ 8 - 0
docs-src/pages/ja/cookbook/s08-compress-response.md

@@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
 });
 ```
 
+## 圧縮を止める
+
+```cpp
+svr.set_compress(false);
+```
+
+ハンドラのレスポンスは、通常のボディもストリーミングも、そのまま送られます。HTTPSでは、攻撃者が選べる文字列とCSRFトークンなどの秘密が同じレスポンスに載ると、圧縮後の長さから秘密が漏れることがあります(BREACH攻撃)。静的ファイルの圧縮は別に設定します。
+
 ## 静的ファイルは明示的に有効にする
 
 `set_mount_point()`や`Response::set_file_content()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。

+ 10 - 1
httplib.h

@@ -2357,6 +2357,8 @@ public:
 
   Server &set_payload_max_length(size_t length);
 
+  Server &set_compress(bool on);
+
   Server &set_static_file_compression(bool on);
   Server &set_static_file_compression_min_length(size_t length);
   Server &set_static_file_compression_max_length(size_t length);
@@ -2431,6 +2433,7 @@ protected:
   time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
   time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
   size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
+  bool compress_ = true;
   bool static_file_compression_ = false;
   size_t static_file_compression_min_length_ =
       CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH;
@@ -13854,6 +13857,11 @@ inline Server &Server::set_payload_max_length(size_t length) {
   return *this;
 }
 
+inline Server &Server::set_compress(bool on) {
+  compress_ = on;
+  return *this;
+}
+
 inline Server &Server::set_static_file_compression(bool on) {
   static_file_compression_ = on;
   return *this;
@@ -14882,7 +14890,8 @@ inline void Server::apply_ranges(const Request &req, Response &res,
                    "multipart/byteranges; boundary=" + boundary);
   }
 
-  auto type = detail::encoding_type(req, res);
+  auto type =
+      compress_ ? detail::encoding_type(req, res) : detail::EncodingType::None;
 
   if (res.body.empty()) {
     if (res.content_length_ > 0 && req.ranges.size() == 1 &&

+ 39 - 0
test/test.cc

@@ -14908,6 +14908,45 @@ TEST(ContentEncodingTest, PreEncodedResponseIsNotCompressedAgain) {
   EXPECT_EQ("Hello World!", res->body);
 }
 
+// set_compress(false) sends every handler response as it is, buffered or
+// streamed
+TEST(ContentEncodingTest, SetCompressFalseSendsIdentity) {
+  const std::string text(1000, 'a');
+
+  Server svr;
+  svr.set_compress(false);
+  svr.Get("/buffered", [&](const Request &, Response &res) {
+    res.set_content(text, "text/plain");
+  });
+  svr.Get("/streamed", [&](const Request &, Response &res) {
+    res.set_chunked_content_provider("text/plain", [&](size_t, DataSink &sink) {
+      sink.write(text.data(), text.size());
+      sink.done();
+      return true;
+    });
+  });
+
+  auto port = svr.bind_to_any_port(HOST);
+  ASSERT_LT(0, port);
+  thread t = thread([&] { svr.listen_after_bind(); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+  svr.wait_until_ready();
+
+  Client cli(HOST, port);
+  cli.set_decompress(false);
+  for (auto path : {"/buffered", "/streamed"}) {
+    SCOPED_TRACE(path);
+    auto res = cli.Get(path, Headers{{"Accept-Encoding", "gzip"}});
+    ASSERT_TRUE(res) << "Error: " << to_string(res.error());
+    EXPECT_FALSE(res->has_header("Content-Encoding"));
+    EXPECT_EQ(text, res->body);
+  }
+}
+
 // A chunked provider settles its coding where the headers are written and
 // reuses it at write time. Both ends of that have to hold: a handler's own
 // Content-Encoding suppresses the coding, and a response without one is still