http2: don't sniff Content-type in Server when X-Content-Type-Options:nosniff

The header X-Content-Type-Options:nosniff is an explicit directive that
content-type should not be sniffed.

----

https://fetch.spec.whatwg.org/#x-content-type-options-header
defines the X-Content-Type-Options header.

["Polyglots: Crossing Origins by Crossing Formats"](http://citeseerx.ist.psu.edu
/viewdoc/download?doi=10.1.1.905.2946&rep=rep1&type=pdf)
explains Polyglot attacks in more detail.

Fixes golang/go#24795

Change-Id: Ibcc2d6a561394392ad0bf112eecc01c43823a2a2
Reviewed-on: https://go-review.googlesource.com/107295
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/http2/server.go b/http2/server.go
index 3e023dc..72f65c8 100644
--- a/http2/server.go
+++ b/http2/server.go
@@ -2324,7 +2324,15 @@
 		}
 		_, hasContentType := rws.snapHeader["Content-Type"]
 		if !hasContentType && bodyAllowedForStatus(rws.status) && len(p) > 0 {
-			ctype = http.DetectContentType(p)
+			if cto := rws.snapHeader.Get("X-Content-Type-Options"); strings.EqualFold("nosniff", cto) {
+				// nosniff is an explicit directive not to guess a content-type.
+				// Content-sniffing is no less susceptible to polyglot attacks via
+				// hosted content when done on the server.
+				ctype = "application/octet-stream"
+				rws.conn.logf("http2: WriteHeader called with X-Content-Type-Options:nosniff but no Content-Type")
+			} else {
+				ctype = http.DetectContentType(p)
+			}
 		}
 		var date string
 		if _, ok := rws.snapHeader["Date"]; !ok {
diff --git a/http2/server_test.go b/http2/server_test.go
index c5d8459..4d66a4b 100644
--- a/http2/server_test.go
+++ b/http2/server_test.go
@@ -1760,6 +1760,42 @@
 	})
 }
 
+func TestServer_Response_Nosniff_WithoutContentType(t *testing.T) {
+	const msg = "<html>this is HTML."
+	testServerResponse(t, func(w http.ResponseWriter, r *http.Request) error {
+		w.Header().Set("X-Content-Type-Options", "nosniff")
+		w.WriteHeader(200)
+		io.WriteString(w, msg)
+		return nil
+	}, func(st *serverTester) {
+		getSlash(st)
+		hf := st.wantHeaders()
+		if hf.StreamEnded() {
+			t.Fatal("don't want END_STREAM, expecting data")
+		}
+		if !hf.HeadersEnded() {
+			t.Fatal("want END_HEADERS flag")
+		}
+		goth := st.decodeHeader(hf.HeaderBlockFragment())
+		wanth := [][2]string{
+			{":status", "200"},
+			{"x-content-type-options", "nosniff"},
+			{"content-type", "application/octet-stream"},
+			{"content-length", strconv.Itoa(len(msg))},
+		}
+		if !reflect.DeepEqual(goth, wanth) {
+			t.Errorf("Got headers %v; want %v", goth, wanth)
+		}
+		df := st.wantData()
+		if !df.StreamEnded() {
+			t.Error("expected DATA to have END_STREAM flag")
+		}
+		if got := string(df.Data()); got != msg {
+			t.Errorf("got DATA %q; want %q", got, msg)
+		}
+	})
+}
+
 func TestServer_Response_TransferEncoding_chunked(t *testing.T) {
 	const msg = "hi"
 	testServerResponse(t, func(w http.ResponseWriter, r *http.Request) error {