github.com/hernad/nomad@v1.6.112/helper/noxssrw/noxssrw.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 // Package noxssrw (No XSS ResponseWriter) behaves like the Go standard 5 // library's ResponseWriter by detecting the Content-Type of a response if it 6 // has not been explicitly set. However, unlike the standard library's 7 // implementation, this implementation will never return the "text/html" 8 // Content-Type and instead return "text/plain". 9 package noxssrw 10 11 import ( 12 "net/http" 13 "strings" 14 ) 15 16 var ( 17 // DefaultUnsafeTypes are Content-Types that browsers will render as hypertext. 18 // Any Content-Types that allow Javascript or remote resource fetching must be 19 // converted to a Content-Type that prevents evaluation. 20 // 21 // Types are prefix matched to avoid comparing against specific 22 // character sets (eg "text/html; charset=utf-8") which may be user 23 // controlled. 24 DefaultUnsafeTypes = map[string]string{ 25 "text/html": "text/plain", 26 "text/xhtml": "text/plain", 27 "text/xhtml+xml": "text/plain", 28 } 29 30 // DefaultHeaders contain CORS headers meant to prevent the execution 31 // of Javascript in compliant browsers. 32 DefaultHeaders = map[string]string{ 33 "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; sandbox", 34 "X-Content-Type-Options": "nosniff", 35 "X-XSS-Protection": "1; mode=block", 36 } 37 ) 38 39 // NoXSSResponseWriter implements http.ResponseWriter but prevents renderable 40 // Content-Types from being automatically detected. Create with 41 // NewResponseWriter. 42 type NoXSSResponseWriter struct { 43 // TypeMap maps types unsafe for untrusted content to their safe 44 // version; may be replaced but not mutated. 45 TypeMap map[string]string 46 47 // DefaultHeaders to set on first write if they are not already 48 // explicitly set. 49 DefaultHeaders map[string]string 50 51 // buffer up to 512 bytes before detecting Content-Type and writing 52 // response. 53 buf []byte 54 55 // subsequentWrite is true after the first Write is called 56 subsequentWrite bool 57 58 // flushed is true if Content-Type has been set and Writes may be 59 // passed through. 60 flushed bool 61 62 // original ResponseWriter being wrapped 63 orig http.ResponseWriter 64 } 65 66 // Header returns the header map that will be sent by 67 // WriteHeader. The Header map also is the mechanism with which 68 // Handlers can set HTTP trailers. 69 // 70 // Changing the header map after a call to WriteHeader (or 71 // Write) has no effect unless the modified headers are 72 // trailers. 73 // 74 // There are two ways to set Trailers. The preferred way is to 75 // predeclare in the headers which trailers you will later 76 // send by setting the "Trailer" header to the names of the 77 // trailer keys which will come later. In this case, those 78 // keys of the Header map are treated as if they were 79 // trailers. See the example. The second way, for trailer 80 // keys not known to the Handler until after the first Write, 81 // is to prefix the Header map keys with the TrailerPrefix 82 // constant value. See TrailerPrefix. 83 // 84 // To suppress automatic response headers (such as "Date"), set 85 // their value to nil. 86 func (w *NoXSSResponseWriter) Header() http.Header { 87 return w.orig.Header() 88 } 89 90 // Write writes the data to the connection as part of an HTTP reply. 91 // 92 // If WriteHeader has not yet been called, Write calls 93 // WriteHeader(http.StatusOK) before writing the data. If the Header 94 // does not contain a Content-Type line, Write adds a Content-Type set 95 // to the result of passing the initial 512 bytes of written data to 96 // DetectContentType. Additionally, if the total size of all written 97 // data is under a few KB and there are no Flush calls, the 98 // Content-Length header is added automatically. 99 // 100 // Depending on the HTTP protocol version and the client, calling 101 // Write or WriteHeader may prevent future reads on the 102 // Request.Body. For HTTP/1.x requests, handlers should read any 103 // needed request body data before writing the response. Once the 104 // headers have been flushed (due to either an explicit Flusher.Flush 105 // call or writing enough data to trigger a flush), the request body 106 // may be unavailable. For HTTP/2 requests, the Go HTTP server permits 107 // handlers to continue to read the request body while concurrently 108 // writing the response. However, such behavior may not be supported 109 // by all HTTP/2 clients. Handlers should read before writing if 110 // possible to maximize compatibility. 111 func (w *NoXSSResponseWriter) Write(p []byte) (int, error) { 112 headers := w.Header() 113 // If first write, set any unset default headers. Do this on first write 114 // to allow overriding the default set of headers. 115 if !w.subsequentWrite { 116 for k, v := range w.DefaultHeaders { 117 if headers.Get(k) == "" { 118 headers.Set(k, v) 119 } 120 } 121 w.subsequentWrite = true 122 } 123 124 // If already flushed, write-through and short-circuit 125 if w.flushed { 126 return w.orig.Write(p) 127 } 128 129 // < 512 bytes available, buffer and wait for closing or a subsequent 130 // request 131 if len(w.buf)+len(p) < 512 { 132 w.buf = append(w.buf, p...) 133 return len(p), nil 134 } 135 136 // >= 512 bytes available, set the Content-Type and flush. 137 all := append(w.buf, p...) //nolint:gocritic 138 contentType := http.DetectContentType(all) 139 140 // Prefix match to exclude the character set which may be user 141 // controlled. 142 for prefix, safe := range w.TypeMap { 143 if strings.HasPrefix(contentType, prefix) { 144 contentType = safe 145 break 146 } 147 } 148 149 // Set the Content-Type iff it was not already explicitly set 150 if headers.Get("Content-Type") == "" { 151 headers.Set("Content-Type", contentType) 152 } 153 154 // Write the buffer 155 n, err := w.orig.Write(w.buf) 156 if err != nil { 157 // Throw away part of buffer written successfully and 158 // inform caller p was not written at all 159 w.buf = w.buf[:n] 160 return 0, err 161 } 162 163 // Headers and buffer were written, this writer has been 164 // flushed and can be a passthrough 165 w.flushed = true 166 167 // Write p 168 return w.orig.Write(p) 169 } 170 171 // Close and flush the writer. Necessary for responses that never reached 512 172 // bytes. 173 func (w *NoXSSResponseWriter) Close() (int, error) { 174 // If the buffer was already flushed this is a noop 175 if w.flushed { 176 return 0, nil 177 } 178 179 // Prefix match to exclude the character set which may be user 180 // controlled. 181 contentType := http.DetectContentType(w.buf) 182 for prefix, safe := range w.TypeMap { 183 if strings.HasPrefix(contentType, prefix) { 184 contentType = safe 185 break 186 } 187 } 188 189 // Set the Content-Type iff it was not already explicitly set 190 if headers := w.Header(); headers.Get("Content-Type") == "" { 191 headers.Set("Content-Type", contentType) 192 } 193 194 // Write the buffer 195 return w.orig.Write(w.buf) 196 } 197 198 // WriteHeader sends an HTTP response header with the provided 199 // status code. 200 // 201 // If WriteHeader is not called explicitly, the first call to Write 202 // will trigger an implicit WriteHeader(http.StatusOK). 203 // Thus explicit calls to WriteHeader are mainly used to 204 // send error codes. 205 // 206 // The provided code must be a valid HTTP 1xx-5xx status code. 207 // Only one header may be written. Go does not currently 208 // support sending user-defined 1xx informational headers, 209 // with the exception of 100-continue response header that the 210 // Server sends automatically when the Request.Body is read. 211 func (w *NoXSSResponseWriter) WriteHeader(statusCode int) { 212 w.orig.WriteHeader(statusCode) 213 } 214 215 // NewResponseWriter creates a new ResponseWriter and Close func which will 216 // prevent Go's http.ResponseWriter default behavior of detecting the 217 // Content-Type. 218 // 219 // The Close func must be called to ensure that responses < 512 bytes are 220 // flushed as up to 512 bytes are buffered without flushing. 221 func NewResponseWriter(orig http.ResponseWriter) (http.ResponseWriter, func() (int, error)) { 222 w := &NoXSSResponseWriter{ 223 TypeMap: DefaultUnsafeTypes, 224 DefaultHeaders: DefaultHeaders, 225 buf: make([]byte, 0, 512), 226 orig: orig, 227 } 228 229 return w, w.Close 230 }