open-nomad/helper/noxssrw/noxssrw.go

231 lines
7.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package noxssrw (No XSS ResponseWriter) behaves like the Go standard
// library's ResponseWriter by detecting the Content-Type of a response if it
// has not been explicitly set. However, unlike the standard library's
// implementation, this implementation will never return the "text/html"
// Content-Type and instead return "text/plain".
package noxssrw
import (
"net/http"
"strings"
)
var (
// DefaultUnsafeTypes are Content-Types that browsers will render as hypertext.
// Any Content-Types that allow Javascript or remote resource fetching must be
// converted to a Content-Type that prevents evaluation.
//
// Types are prefix matched to avoid comparing against specific
// character sets (eg "text/html; charset=utf-8") which may be user
// controlled.
DefaultUnsafeTypes = map[string]string{
"text/html": "text/plain",
"text/xhtml": "text/plain",
"text/xhtml+xml": "text/plain",
}
// DefaultHeaders contain CORS headers meant to prevent the execution
// of Javascript in compliant browsers.
DefaultHeaders = map[string]string{
"Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox",
"X-Content-Type-Options": "nosniff",
"X-XSS-Protection": "1; mode=block",
}
)
// NoXSSResponseWriter implements http.ResponseWriter but prevents renderable
// Content-Types from being automatically detected. Create with
// NewResponseWriter.
type NoXSSResponseWriter struct {
// TypeMap maps types unsafe for untrusted content to their safe
// version; may be replaced but not mutated.
TypeMap map[string]string
// DefaultHeaders to set on first write if they are not already
// explicitly set.
DefaultHeaders map[string]string
// buffer up to 512 bytes before detecting Content-Type and writing
// response.
buf []byte
// subsequentWrite is true after the first Write is called
subsequentWrite bool
// flushed is true if Content-Type has been set and Writes may be
// passed through.
flushed bool
// original ResponseWriter being wrapped
orig http.ResponseWriter
}
// Header returns the header map that will be sent by
// WriteHeader. The Header map also is the mechanism with which
// Handlers can set HTTP trailers.
//
// Changing the header map after a call to WriteHeader (or
// Write) has no effect unless the modified headers are
// trailers.
//
// There are two ways to set Trailers. The preferred way is to
// predeclare in the headers which trailers you will later
// send by setting the "Trailer" header to the names of the
// trailer keys which will come later. In this case, those
// keys of the Header map are treated as if they were
// trailers. See the example. The second way, for trailer
// keys not known to the Handler until after the first Write,
// is to prefix the Header map keys with the TrailerPrefix
// constant value. See TrailerPrefix.
//
// To suppress automatic response headers (such as "Date"), set
// their value to nil.
func (w *NoXSSResponseWriter) Header() http.Header {
return w.orig.Header()
}
// Write writes the data to the connection as part of an HTTP reply.
//
// If WriteHeader has not yet been called, Write calls
// WriteHeader(http.StatusOK) before writing the data. If the Header
// does not contain a Content-Type line, Write adds a Content-Type set
// to the result of passing the initial 512 bytes of written data to
// DetectContentType. Additionally, if the total size of all written
// data is under a few KB and there are no Flush calls, the
// Content-Length header is added automatically.
//
// Depending on the HTTP protocol version and the client, calling
// Write or WriteHeader may prevent future reads on the
// Request.Body. For HTTP/1.x requests, handlers should read any
// needed request body data before writing the response. Once the
// headers have been flushed (due to either an explicit Flusher.Flush
// call or writing enough data to trigger a flush), the request body
// may be unavailable. For HTTP/2 requests, the Go HTTP server permits
// handlers to continue to read the request body while concurrently
// writing the response. However, such behavior may not be supported
// by all HTTP/2 clients. Handlers should read before writing if
// possible to maximize compatibility.
func (w *NoXSSResponseWriter) Write(p []byte) (int, error) {
headers := w.Header()
// If first write, set any unset default headers. Do this on first write
// to allow overriding the default set of headers.
if !w.subsequentWrite {
for k, v := range w.DefaultHeaders {
if headers.Get(k) == "" {
headers.Set(k, v)
}
}
w.subsequentWrite = true
}
// If already flushed, write-through and short-circuit
if w.flushed {
return w.orig.Write(p)
}
// < 512 bytes available, buffer and wait for closing or a subsequent
// request
if len(w.buf)+len(p) < 512 {
w.buf = append(w.buf, p...)
return len(p), nil
}
// >= 512 bytes available, set the Content-Type and flush.
all := append(w.buf, p...) //nolint:gocritic
contentType := http.DetectContentType(all)
// Prefix match to exclude the character set which may be user
// controlled.
for prefix, safe := range w.TypeMap {
if strings.HasPrefix(contentType, prefix) {
contentType = safe
break
}
}
// Set the Content-Type iff it was not already explicitly set
if headers.Get("Content-Type") == "" {
headers.Set("Content-Type", contentType)
}
// Write the buffer
n, err := w.orig.Write(w.buf)
if err != nil {
// Throw away part of buffer written successfully and
// inform caller p was not written at all
w.buf = w.buf[:n]
return 0, err
}
// Headers and buffer were written, this writer has been
// flushed and can be a passthrough
w.flushed = true
// Write p
return w.orig.Write(p)
}
// Close and flush the writer. Necessary for responses that never reached 512
// bytes.
func (w *NoXSSResponseWriter) Close() (int, error) {
// If the buffer was already flushed this is a noop
if w.flushed {
return 0, nil
}
// Prefix match to exclude the character set which may be user
// controlled.
contentType := http.DetectContentType(w.buf)
for prefix, safe := range w.TypeMap {
if strings.HasPrefix(contentType, prefix) {
contentType = safe
break
}
}
// Set the Content-Type iff it was not already explicitly set
if headers := w.Header(); headers.Get("Content-Type") == "" {
headers.Set("Content-Type", contentType)
}
// Write the buffer
return w.orig.Write(w.buf)
}
// WriteHeader sends an HTTP response header with the provided
// status code.
//
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).
// Thus explicit calls to WriteHeader are mainly used to
// send error codes.
//
// The provided code must be a valid HTTP 1xx-5xx status code.
// Only one header may be written. Go does not currently
// support sending user-defined 1xx informational headers,
// with the exception of 100-continue response header that the
// Server sends automatically when the Request.Body is read.
func (w *NoXSSResponseWriter) WriteHeader(statusCode int) {
w.orig.WriteHeader(statusCode)
}
// NewResponseWriter creates a new ResponseWriter and Close func which will
// prevent Go's http.ResponseWriter default behavior of detecting the
// Content-Type.
//
// The Close func must be called to ensure that responses < 512 bytes are
// flushed as up to 512 bytes are buffered without flushing.
func NewResponseWriter(orig http.ResponseWriter) (http.ResponseWriter, func() (int, error)) {
w := &NoXSSResponseWriter{
TypeMap: DefaultUnsafeTypes,
DefaultHeaders: DefaultHeaders,
buf: make([]byte, 0, 512),
orig: orig,
}
return w, w.Close
}