Commit f73e4c9e authored by Filippo Valsorda's avatar Filippo Valsorda Committed by Brad Fitzpatrick

http2: add X-Content-Type-Options automatically to prevent sniffing

When a Content-Type that triggers content sniffing in old (but still in
significant use) browsers is sent, add the
X-Content-Type-Options: nosniff header, unless explicitly disabled.

Expose httpguts.SniffedContentType for use in the HTTP 1 implementation.

Will be tested by net/http.TestNoSniffHeader_h2.

Updates golang/go#24513

Change-Id: Id1ffea867a496393cb52c5a9f45af97d4b2fcf12
Reviewed-on: https://go-review.googlesource.com/112015
Run-TryBot: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: 's avatarBrad Fitzpatrick <bradfitz@golang.org>
parent 3a7846fe
...@@ -14,6 +14,21 @@ import ( ...@@ -14,6 +14,21 @@ import (
"strings" "strings"
) )
// SniffedContentType reports whether ct is a Content-Type that is known
// to cause client-side content sniffing.
//
// This provides just a partial implementation of mime.ParseMediaType
// with the assumption that the Content-Type is not attacker controlled.
func SniffedContentType(ct string) bool {
if i := strings.Index(ct, ";"); i != -1 {
ct = ct[:i]
}
ct = strings.ToLower(strings.TrimSpace(ct))
return ct == "text/plain" || ct == "application/octet-stream" ||
ct == "application/unknown" || ct == "unknown/unknown" || ct == "*/*" ||
!strings.Contains(ct, "/")
}
// ValidTrailerHeader reports whether name is a valid header field name to appear // ValidTrailerHeader reports whether name is a valid header field name to appear
// in trailers. // in trailers.
// See RFC 7230, Section 4.1.2 // See RFC 7230, Section 4.1.2
......
...@@ -2309,6 +2309,7 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { ...@@ -2309,6 +2309,7 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) {
isHeadResp := rws.req.Method == "HEAD" isHeadResp := rws.req.Method == "HEAD"
if !rws.sentHeader { if !rws.sentHeader {
rws.sentHeader = true rws.sentHeader = true
var ctype, clen string var ctype, clen string
if clen = rws.snapHeader.Get("Content-Length"); clen != "" { if clen = rws.snapHeader.Get("Content-Length"); clen != "" {
rws.snapHeader.Del("Content-Length") rws.snapHeader.Del("Content-Length")
...@@ -2322,6 +2323,7 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { ...@@ -2322,6 +2323,7 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) {
if clen == "" && rws.handlerDone && bodyAllowedForStatus(rws.status) && (len(p) > 0 || !isHeadResp) { if clen == "" && rws.handlerDone && bodyAllowedForStatus(rws.status) && (len(p) > 0 || !isHeadResp) {
clen = strconv.Itoa(len(p)) clen = strconv.Itoa(len(p))
} }
_, hasContentType := rws.snapHeader["Content-Type"] _, hasContentType := rws.snapHeader["Content-Type"]
if !hasContentType && bodyAllowedForStatus(rws.status) && len(p) > 0 { if !hasContentType && bodyAllowedForStatus(rws.status) && len(p) > 0 {
if cto := rws.snapHeader.Get("X-Content-Type-Options"); strings.EqualFold("nosniff", cto) { if cto := rws.snapHeader.Get("X-Content-Type-Options"); strings.EqualFold("nosniff", cto) {
...@@ -2334,6 +2336,20 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { ...@@ -2334,6 +2336,20 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) {
ctype = http.DetectContentType(p) ctype = http.DetectContentType(p)
} }
} }
var noSniff bool
if bodyAllowedForStatus(rws.status) && (rws.sentContentLen > 0 || len(p) > 0) {
// If the content type triggers client-side sniffing on old browsers,
// attach a X-Content-Type-Options header if not present (or explicitly nil).
if _, ok := rws.snapHeader["X-Content-Type-Options"]; !ok {
if hasContentType {
noSniff = httpguts.SniffedContentType(rws.snapHeader.Get("Content-Type"))
} else if ctype != "" {
noSniff = httpguts.SniffedContentType(ctype)
}
}
}
var date string var date string
if _, ok := rws.snapHeader["Date"]; !ok { if _, ok := rws.snapHeader["Date"]; !ok {
// TODO(bradfitz): be faster here, like net/http? measure. // TODO(bradfitz): be faster here, like net/http? measure.
...@@ -2352,6 +2368,7 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) { ...@@ -2352,6 +2368,7 @@ func (rws *responseWriterState) writeChunk(p []byte) (n int, err error) {
endStream: endStream, endStream: endStream,
contentType: ctype, contentType: ctype,
contentLength: clen, contentLength: clen,
noSniff: noSniff,
date: date, date: date,
}) })
if err != nil { if err != nil {
......
...@@ -1810,6 +1810,7 @@ func TestServer_Response_TransferEncoding_chunked(t *testing.T) { ...@@ -1810,6 +1810,7 @@ func TestServer_Response_TransferEncoding_chunked(t *testing.T) {
{":status", "200"}, {":status", "200"},
{"content-type", "text/plain; charset=utf-8"}, {"content-type", "text/plain; charset=utf-8"},
{"content-length", strconv.Itoa(len(msg))}, {"content-length", strconv.Itoa(len(msg))},
{"x-content-type-options", "nosniff"},
} }
if !reflect.DeepEqual(goth, wanth) { if !reflect.DeepEqual(goth, wanth) {
t.Errorf("Got headers %v; want %v", goth, wanth) t.Errorf("Got headers %v; want %v", goth, wanth)
...@@ -1998,6 +1999,7 @@ func TestServer_Response_LargeWrite(t *testing.T) { ...@@ -1998,6 +1999,7 @@ func TestServer_Response_LargeWrite(t *testing.T) {
wanth := [][2]string{ wanth := [][2]string{
{":status", "200"}, {":status", "200"},
{"content-type", "text/plain; charset=utf-8"}, // sniffed {"content-type", "text/plain; charset=utf-8"}, // sniffed
{"x-content-type-options", "nosniff"},
// and no content-length // and no content-length
} }
if !reflect.DeepEqual(goth, wanth) { if !reflect.DeepEqual(goth, wanth) {
...@@ -2212,6 +2214,7 @@ func TestServer_Response_Automatic100Continue(t *testing.T) { ...@@ -2212,6 +2214,7 @@ func TestServer_Response_Automatic100Continue(t *testing.T) {
{":status", "200"}, {":status", "200"},
{"content-type", "text/plain; charset=utf-8"}, {"content-type", "text/plain; charset=utf-8"},
{"content-length", strconv.Itoa(len(reply))}, {"content-length", strconv.Itoa(len(reply))},
{"x-content-type-options", "nosniff"},
} }
if !reflect.DeepEqual(goth, wanth) { if !reflect.DeepEqual(goth, wanth) {
t.Errorf("Got headers %v; want %v", goth, wanth) t.Errorf("Got headers %v; want %v", goth, wanth)
...@@ -2935,6 +2938,7 @@ func testServerWritesTrailers(t *testing.T, withFlush bool) { ...@@ -2935,6 +2938,7 @@ func testServerWritesTrailers(t *testing.T, withFlush bool) {
{"trailer", "Transfer-Encoding, Content-Length, Trailer"}, {"trailer", "Transfer-Encoding, Content-Length, Trailer"},
{"content-type", "text/plain; charset=utf-8"}, {"content-type", "text/plain; charset=utf-8"},
{"content-length", "5"}, {"content-length", "5"},
{"x-content-type-options", "nosniff"},
} }
if !reflect.DeepEqual(goth, wanth) { if !reflect.DeepEqual(goth, wanth) {
t.Errorf("Header mismatch.\n got: %v\nwant: %v", goth, wanth) t.Errorf("Header mismatch.\n got: %v\nwant: %v", goth, wanth)
...@@ -3326,6 +3330,7 @@ func TestServerNoDuplicateContentType(t *testing.T) { ...@@ -3326,6 +3330,7 @@ func TestServerNoDuplicateContentType(t *testing.T) {
{":status", "200"}, {":status", "200"},
{"content-type", ""}, {"content-type", ""},
{"content-length", "41"}, {"content-length", "41"},
{"x-content-type-options", "nosniff"},
} }
if !reflect.DeepEqual(headers, want) { if !reflect.DeepEqual(headers, want) {
t.Errorf("Headers mismatch.\n got: %q\nwant: %q\n", headers, want) t.Errorf("Headers mismatch.\n got: %q\nwant: %q\n", headers, want)
......
...@@ -145,9 +145,10 @@ func TestTransport(t *testing.T) { ...@@ -145,9 +145,10 @@ func TestTransport(t *testing.T) {
t.Errorf("Status = %q; want %q", g, w) t.Errorf("Status = %q; want %q", g, w)
} }
wantHeader := http.Header{ wantHeader := http.Header{
"Content-Length": []string{"3"}, "Content-Length": []string{"3"},
"Content-Type": []string{"text/plain; charset=utf-8"}, "X-Content-Type-Options": []string{"nosniff"},
"Date": []string{"XXX"}, // see cleanDate "Content-Type": []string{"text/plain; charset=utf-8"},
"Date": []string{"XXX"}, // see cleanDate
} }
cleanDate(res) cleanDate(res)
if !reflect.DeepEqual(res.Header, wantHeader) { if !reflect.DeepEqual(res.Header, wantHeader) {
......
...@@ -186,6 +186,7 @@ type writeResHeaders struct { ...@@ -186,6 +186,7 @@ type writeResHeaders struct {
date string date string
contentType string contentType string
contentLength string contentLength string
noSniff bool
} }
func encKV(enc *hpack.Encoder, k, v string) { func encKV(enc *hpack.Encoder, k, v string) {
...@@ -222,6 +223,9 @@ func (w *writeResHeaders) writeFrame(ctx writeContext) error { ...@@ -222,6 +223,9 @@ func (w *writeResHeaders) writeFrame(ctx writeContext) error {
if w.contentLength != "" { if w.contentLength != "" {
encKV(enc, "content-length", w.contentLength) encKV(enc, "content-length", w.contentLength)
} }
if w.noSniff {
encKV(enc, "x-content-type-options", "nosniff")
}
if w.date != "" { if w.date != "" {
encKV(enc, "date", w.date) encKV(enc, "date", w.date)
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment