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 {