228 lines
7.5 KiB
Go
228 lines
7.5 KiB
Go
// 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
|
|
}
|