Repository: tiny-http/tiny-http (Rust)

Issue:
HTTP Request Smuggling Hardening
Posting it here for community patches after talking with the maintainers privately.


Issue: tiny-http doesn't prevent Request Smuggling attacks (CE:TL,TL:TL) where a frontend proxy might allow/pass malformed Transfer Encoding headers but tiny-http will normalise them 


### Steps to Reproduce
* Use a vulnerable version of HAProxy in the following setup (https://nathandavison.com/blog/haproxy-http-request-smuggling) as a frontend
* Use an example framework such as Rouille (which uses tiny-http) (https://github.com/tomaka/rouille/blob/master/examples/login-session.rs) as a backend
* Now send a request similar to the following

```
POST /login HTTP/1.1
Host: test.local
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
Origin: http://dvws.local
Connection: close
Referer: http://test.local/
Upgrade-Insecure-Requests: 1
DNT: 1
Transfer-Encoding : chunked

19
login=admin&password=xxxx
1
Z
Q

```
In the above example, the request is sent with the Transfer encoding header having extra spaces (Transfer-Encoding : chunked). This is in violation of RFC 7230. tiny-http is normalizing the `Transfer-Encoding : chunked` header, hence we get the below reponse. 

```
HTTP/1.1 400 Bad Request
Server: tiny-http (Rust)
Date: Tue, 16 Jun 2020 11:13:12 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 163

{"description":"could not read the body from the request, or could not execute the CGI program","cause":{"description":"Error while decoding chunks","cause":null}}
```

More variations of this exist:
```
Transfer-Encoding: "chunked"
 Transfer-Encoding: chunked
Transfer-Encoding: 'chunked'
Transfer-Encoding: chunk
Transfer-Encoding: chunked
Transfer-Encoding: chunked
Transfer-Encoding: chunked
Transfer-Encoding: chunked
Transfer-Encoding: chunked
Transfer-Encoding: chunked
Transfer-Encoding : chunked
Transfer-Encoding:chunked
````
You can also use https://github.com/defparam/smuggler to fully check what header variations are allowed.



Discussion:

Patch:
diff --git a/src/common.rs b/src/common.rs
index 94ffd6612..b8050b94e 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -212,7 +212,7 @@ impl Display for Header {
 
 /// Field of a header (eg. `Content-Type`, `Content-Length`, etc.)
 ///
-/// Comparaison between two `HeaderField`s ignores case.
+/// Comparison between two `HeaderField`s ignores case.
 #[derive(Debug, Clone)]
 pub struct HeaderField(AsciiString);
 
@@ -239,9 +239,13 @@ impl FromStr for HeaderField {
     type Err = ();
 
     fn from_str(s: &str) -> Result<HeaderField, ()> {
-        AsciiString::from_ascii(s.trim())
-            .map(HeaderField)
-            .map_err(|_| ())
+        if s.contains(char::is_whitespace) {
+            Err(())
+        } else {
+            AsciiString::from_ascii(s)
+                .map(HeaderField)
+                .map_err(|_| ())
+        }
     }
 }
 
@@ -461,4 +465,18 @@ mod test {
         assert!(header.field.equiv(&"time"));
         assert!(header.value.as_str() == "20: 34");
     }
+
+    // This tests reslstance to RUSTSEC-2020-0031: "HTTP Request smuggling
+    // through malformed Transfer Encoding headers"
+    // (https://rustsec.org/advisories/RUSTSEC-2020-0031.html).
+    #[test]
+    fn test_strict_headers() {
+        assert!("Transfer-Encoding : chunked".parse::<Header>().is_err());
+        assert!(" Transfer-Encoding: chunked".parse::<Header>().is_err());
+        assert!("Transfer Encoding: chunked".parse::<Header>().is_err());
+        assert!(" Transfer\tEncoding : chunked".parse::<Header>().is_err());
+        assert!("Transfer-Encoding: chunked".parse::<Header>().is_ok());
+        assert!("Transfer-Encoding: chunked ".parse::<Header>().is_ok());
+        assert!("Transfer-Encoding:   chunked ".parse::<Header>().is_ok());
+    }
 }


Test patch:
diff --git a/tests/input-tests.rs b/tests/input-tests.rs
index 85d59db8a..78db98137 100644
--- a/tests/input-tests.rs
+++ b/tests/input-tests.rs
@@ -81,3 +81,15 @@ fn unsupported_expect_header() {
     client.read_to_string(&mut content).unwrap();
     assert!(&content[9..].starts_with("417")); // 417 status code
 }
+
+#[test]
+fn invalid_header_name() {
+    let mut client = support::new_client_to_hello_world_server();
+
+    // note the space hidden in the Content-Length, which is invalid
+    (write!(client, "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Type: text/plain; charset=utf8\r\nContent-Length : 5\r\n\r\nhello")).unwrap();
+
+    let mut content = String::new();
+    client.read_to_string(&mut content).unwrap();
+    assert!(&content[9..].starts_with("400 Bad Request")); // 400 status code
+}