Repository: actix/actix-web (rust)

Issue:
CVE-2020-8927 in `brotli-sys` dependency
## Expected Behavior
Actix and its dependencies should have no known vulnerabilities unpatched

## Current Behavior
Actix is affected by [CVE-2020-8927](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-8927), buffer overflow in the Brotli C library, through `brotli-sys` crate.

## Possible Solution
There is no updated release of `brotli-sys` that fixes the issue ([it has been reported upstream](https://github.com/bitemyapp/brotli2-rs/issues/45)). There are several abandoned PRs updating to newer Brotli, so it looks like a new release will not be cut anytime soon.

The [`brotli` crate](https://crates.io/crates/brotli) provides a pure Rust implementation of Brotli compression and decompression. It is used in production by Dropbox and is not affected by the issue (and could not be affected because it's written in safe Rust). I suggest switching to the `brotli` crate as a mitigation.

## Steps to Reproduce (for bugs)
1. `cargo install cargo-audit`
2. `cd actix-web`
3. `cargo audit`

The dependency tree leading to the affected `brotli-sys` crate is:
```
brotli-sys 0.3.2
└── brotli2 0.3.2
    ├── awc 3.0.0-beta.14
    │   ├── actix-web-actors 4.0.0-beta.8
    │   ├── actix-web 4.0.0-beta.15
    │   │   ├── awc 3.0.0-beta.14
    │   │   ├── actix-web-codegen 0.5.0-beta.6
    │   │   │   └── actix-web 4.0.0-beta.15
    │   │   ├── actix-web-actors 4.0.0-beta.8
    │   │   ├── actix-test 0.1.0-beta.9
    │   │   │   ├── awc 3.0.0-beta.14
    │   │   │   ├── actix-web-codegen 0.5.0-beta.6
    │   │   │   ├── actix-web-actors 4.0.0-beta.8
    │   │   │   ├── actix-web 4.0.0-beta.15
    │   │   │   └── actix-files 0.6.0-beta.10
    │   │   ├── actix-multipart 0.4.0-beta.10
    │   │   ├── actix-http-test 3.0.0-beta.9
    │   │   │   ├── awc 3.0.0-beta.14
    │   │   │   ├── actix-test 0.1.0-beta.9
    │   │   │   └── actix-http 3.0.0-beta.16
    │   │   │       ├── awc 3.0.0-beta.14
    │   │   │       ├── actix-web-actors 4.0.0-beta.8
    │   │   │       ├── actix-web 4.0.0-beta.15
    │   │   │       ├── actix-test 0.1.0-beta.9
    │   │   │       ├── actix-multipart 0.4.0-beta.10
    │   │   │       ├── actix-http-test 3.0.0-beta.9
    │   │   │       └── actix-files 0.6.0-beta.10
    │   │   ├── actix-http 3.0.0-beta.16
    │   │   └── actix-files 0.6.0-beta.10
    │   ├── actix-test 0.1.0-beta.9
    │   └── actix-http-test 3.0.0-beta.9
    ├── actix-web 4.0.0-beta.15
    └── actix-http 3.0.0-beta.16
```

* Rust Version (I.e, output of `rustc -V`):
* Actix Web Version: actix-http 2.2.1, latest git.


Pull request description:
migrate to `brotli` crate
<!-- Thanks for considering contributing actix! -->
<!-- Please fill out the following to get your PR reviewed quicker. -->

## PR Type
<!-- What kind of change does this PR make? -->
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
Security Fix


## PR Checklist
<!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. -->

- [ ] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated.
- [ ] A changelog entry has been made for the appropriate packages.
- [x] Format code with the latest stable rustfmt.
- [x] (Team) Label with affected crates and semver status.


## Overview
Closes #2537.
Blocked on https://github.com/dropbox/rust-brotli/pull/66.


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 945d72ef..39f2ac32 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -109,7 +109,7 @@ actix-files = "0.6.0-beta.14"
 actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
 awc = { version = "3.0.0-beta.18", features = ["openssl"] }
 
-brotli2 = "0.3.2"
+brotli = "3.3.3"
 const-str = "0.3"
 criterion = { version = "0.3", features = ["html_reports"] }
 env_logger = "0.9"
diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml
index 519230aa..df9e1141 100644
--- a/actix-http/Cargo.toml
+++ b/actix-http/Cargo.toml
@@ -33,7 +33,7 @@ openssl = ["actix-tls/accept", "actix-tls/openssl"]
 rustls = ["actix-tls/accept", "actix-tls/rustls"]
 
 # enable compression support
-compress-brotli = ["brotli2", "__compress"]
+compress-brotli = ["brotli", "__compress"]
 compress-gzip = ["flate2", "__compress"]
 compress-zstd = ["zstd", "__compress"]
 
@@ -74,7 +74,7 @@ smallvec = "1.6.1"
 actix-tls = { version = "3.0.0", default-features = false, optional = true }
 
 # compression
-brotli2 = { version="0.3.2", optional = true }
+brotli = { version = "3.3.3", optional = true }
 flate2 = { version = "1.0.13", optional = true }
 zstd = { version = "0.9", optional = true }
 
diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs
index da4b56c6..2ed7be89 100644
--- a/actix-http/src/encoding/decoder.rs
+++ b/actix-http/src/encoding/decoder.rs
@@ -11,9 +11,6 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
 use bytes::Bytes;
 use futures_core::{ready, Stream};
 
-#[cfg(feature = "compress-brotli")]
-use brotli2::write::BrotliDecoder;
-
 #[cfg(feature = "compress-gzip")]
 use flate2::write::{GzDecoder, ZlibDecoder};
 
@@ -48,7 +45,7 @@ where
         let decoder = match encoding {
             #[cfg(feature = "compress-brotli")]
             ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
-                BrotliDecoder::new(Writer::new()),
+                brotli::DecompressorWriter::new(Writer::new(), 8_096),
             ))),
             #[cfg(feature = "compress-gzip")]
             ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
@@ -165,7 +162,7 @@ enum ContentDecoder {
     #[cfg(feature = "compress-gzip")]
     Gzip(Box<GzDecoder<Writer>>),
     #[cfg(feature = "compress-brotli")]
-    Brotli(Box<BrotliDecoder<Writer>>),
+    Brotli(Box<brotli::DecompressorWriter<Writer>>),
     // We need explicit 'static lifetime here because ZstdDecoder need lifetime
     // argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
     #[cfg(feature = "compress-zstd")]
diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs
index 2848ad69..9696da6f 100644
--- a/actix-http/src/encoding/encoder.rs
+++ b/actix-http/src/encoding/encoder.rs
@@ -14,9 +14,6 @@ use derive_more::Display;
 use futures_core::ready;
 use pin_project_lite::pin_project;
 
-#[cfg(feature = "compress-brotli")]
-use brotli2::write::BrotliEncoder;
-
 #[cfg(feature = "compress-gzip")]
 use flate2::write::{GzEncoder, ZlibEncoder};
 
@@ -268,7 +265,7 @@ enum ContentEncoder {
     Gzip(GzEncoder<Writer>),
 
     #[cfg(feature = "compress-brotli")]
-    Brotli(BrotliEncoder<Writer>),
+    Brotli(Box<brotli::CompressorWriter<Writer>>),
 
     // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
     // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
@@ -292,9 +289,7 @@ impl ContentEncoder {
             ))),
 
             #[cfg(feature = "compress-brotli")]
-            ContentEncoding::Brotli => {
-                Some(ContentEncoder::Brotli(BrotliEncoder::new(Writer::new(), 3)))
-            }
+            ContentEncoding::Brotli => Some(ContentEncoder::Brotli(new_brotli_compressor())),
 
             #[cfg(feature = "compress-zstd")]
             ContentEncoding::Zstd => {
@@ -326,8 +321,8 @@ impl ContentEncoder {
     fn finish(self) -> Result<Bytes, io::Error> {
         match self {
             #[cfg(feature = "compress-brotli")]
-            ContentEncoder::Brotli(encoder) => match encoder.finish() {
-                Ok(writer) => Ok(writer.buf.freeze()),
+            ContentEncoder::Brotli(mut encoder) => match encoder.flush() {
+                Ok(()) => Ok(encoder.into_inner().buf.freeze()),
                 Err(err) => Err(err),
             },
 
@@ -392,6 +387,16 @@ impl ContentEncoder {
     }
 }
 
+#[cfg(feature = "compress-brotli")]
+fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
+    Box::new(brotli::CompressorWriter::new(
+        Writer::new(),
+        8 * 1024, // 32 KiB buffer
+        3,        // BROTLI_PARAM_QUALITY
+        22,       // BROTLI_PARAM_LGWIN
+    ))
+}
+
 #[derive(Debug, Display)]
 #[non_exhaustive]
 pub enum EncoderError {
diff --git a/awc/Cargo.toml b/awc/Cargo.toml
index 8accf4d0..16c2083d 100644
--- a/awc/Cargo.toml
+++ b/awc/Cargo.toml
@@ -101,7 +101,7 @@ actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
 actix-utils = "3.0.0"
 actix-web = { version = "4.0.0-beta.20", features = ["openssl"] }
 
-brotli2 = "0.3.2"
+brotli = "3.3.3"
 const-str = "0.3"
 env_logger = "0.9"
 flate2 = "1.0.13"


Test patch:
diff --git a/awc/tests/utils.rs b/awc/tests/utils.rs
index 9a3743d8..2532640c 100644
--- a/awc/tests/utils.rs
+++ b/awc/tests/utils.rs
@@ -41,16 +41,22 @@ pub mod deflate {
 
 pub mod brotli {
     use super::*;
-    use ::brotli2::{read::BrotliDecoder, write::BrotliEncoder};
+    use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder};
 
     pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
-        let mut encoder = BrotliEncoder::new(Vec::new(), 3);
+        let mut encoder = BrotliEncoder::new(
+            Vec::new(),
+            8 * 1024, // 32 KiB buffer
+            3,        // BROTLI_PARAM_QUALITY
+            22,       // BROTLI_PARAM_LGWIN
+        );
         encoder.write_all(bytes.as_ref()).unwrap();
-        encoder.finish().unwrap()
+        encoder.flush().unwrap();
+        encoder.into_inner()
     }
 
     pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
-        let mut decoder = BrotliDecoder::new(bytes.as_ref());
+        let mut decoder = BrotliDecoder::new(bytes.as_ref(), 8_096);
         let mut buf = Vec::new();
         decoder.read_to_end(&mut buf).unwrap();
         buf
diff --git a/tests/utils.rs b/tests/utils.rs
index 9a3743d8..2532640c 100644
--- a/tests/utils.rs
+++ b/tests/utils.rs
@@ -41,16 +41,22 @@ pub mod deflate {
 
 pub mod brotli {
     use super::*;
-    use ::brotli2::{read::BrotliDecoder, write::BrotliEncoder};
+    use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder};
 
     pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
-        let mut encoder = BrotliEncoder::new(Vec::new(), 3);
+        let mut encoder = BrotliEncoder::new(
+            Vec::new(),
+            8 * 1024, // 32 KiB buffer
+            3,        // BROTLI_PARAM_QUALITY
+            22,       // BROTLI_PARAM_LGWIN
+        );
         encoder.write_all(bytes.as_ref()).unwrap();
-        encoder.finish().unwrap()
+        encoder.flush().unwrap();
+        encoder.into_inner()
     }
 
     pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
-        let mut decoder = BrotliDecoder::new(bytes.as_ref());
+        let mut decoder = BrotliDecoder::new(bytes.as_ref(), 8_096);
         let mut buf = Vec::new();
         decoder.read_to_end(&mut buf).unwrap();
         buf