|
|
@ -74,25 +74,74 @@ import (
|
|
|
|
crand "crypto/rand"
|
|
|
|
crand "crypto/rand"
|
|
|
|
"fmt"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"log"
|
|
|
|
|
|
|
|
"maps"
|
|
|
|
"net"
|
|
|
|
"net"
|
|
|
|
"net/http"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"net/url"
|
|
|
|
"path"
|
|
|
|
"path"
|
|
|
|
|
|
|
|
"slices"
|
|
|
|
"strings"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gorilla/csrf"
|
|
|
|
"github.com/gorilla/csrf"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// The default Content-Security-Policy header.
|
|
|
|
// CSP is the value of a Content-Security-Policy header. Keys are CSP
|
|
|
|
var defaultCSP = strings.Join([]string{
|
|
|
|
// directives (like "default-src") and values are source expressions (like
|
|
|
|
`default-src 'self'`, // origin is the only valid source for all content types
|
|
|
|
// "'self'" or "https://tailscale.com"). A nil slice value is allowed for some
|
|
|
|
`script-src 'self'`, // disallow inline javascript
|
|
|
|
// directives like "upgrade-insecure-requests" that don't expect a list of
|
|
|
|
`frame-ancestors 'none'`, // disallow framing of the page
|
|
|
|
// source definitions.
|
|
|
|
`form-action 'self'`, // disallow form submissions to other origins
|
|
|
|
type CSP map[string][]string
|
|
|
|
`base-uri 'self'`, // disallow base URIs from other origins
|
|
|
|
|
|
|
|
`block-all-mixed-content`, // disallow mixed content when serving over HTTPS
|
|
|
|
// DefaultCSP is the recommended CSP to use when not loading resources from
|
|
|
|
`object-src 'self'`, // disallow embedding of resources from other origins
|
|
|
|
// other domains and not embedding the current website. If you need to tweak
|
|
|
|
}, "; ")
|
|
|
|
// the CSP, it is recommended to extend DefaultCSP instead of writing your own
|
|
|
|
|
|
|
|
// from scratch.
|
|
|
|
|
|
|
|
func DefaultCSP() CSP {
|
|
|
|
|
|
|
|
return CSP{
|
|
|
|
|
|
|
|
"default-src": {"self"}, // origin is the only valid source for all content types
|
|
|
|
|
|
|
|
"frame-ancestors": {"none"}, // disallow framing of the page
|
|
|
|
|
|
|
|
"form-action": {"self"}, // disallow form submissions to other origins
|
|
|
|
|
|
|
|
"base-uri": {"self"}, // disallow base URIs from other origins
|
|
|
|
|
|
|
|
// TODO(awly): consider upgrade-insecure-requests in SecureContext
|
|
|
|
|
|
|
|
// instead, as this is deprecated.
|
|
|
|
|
|
|
|
"block-all-mixed-content": nil, // disallow mixed content when serving over HTTPS
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Set sets the values for a given directive. Empty values are allowed, if the
|
|
|
|
|
|
|
|
// directive doesn't expect any (like "upgrade-insecure-requests").
|
|
|
|
|
|
|
|
func (csp CSP) Set(directive string, values ...string) {
|
|
|
|
|
|
|
|
csp[directive] = values
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add adds a source expression to an existing directive.
|
|
|
|
|
|
|
|
func (csp CSP) Add(directive, value string) {
|
|
|
|
|
|
|
|
csp[directive] = append(csp[directive], value)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Del deletes a directive and all its values.
|
|
|
|
|
|
|
|
func (csp CSP) Del(directive string) {
|
|
|
|
|
|
|
|
delete(csp, directive)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (csp CSP) String() string {
|
|
|
|
|
|
|
|
keys := slices.Collect(maps.Keys(csp))
|
|
|
|
|
|
|
|
slices.Sort(keys)
|
|
|
|
|
|
|
|
var s strings.Builder
|
|
|
|
|
|
|
|
for _, k := range keys {
|
|
|
|
|
|
|
|
s.WriteString(k)
|
|
|
|
|
|
|
|
for _, v := range csp[k] {
|
|
|
|
|
|
|
|
// Special values like 'self', 'none', 'unsafe-inline', etc., must
|
|
|
|
|
|
|
|
// be quoted. Do it implicitly as a convenience here.
|
|
|
|
|
|
|
|
if !strings.Contains(v, ".") && len(v) > 1 && v[0] != '\'' && v[len(v)-1] != '\'' {
|
|
|
|
|
|
|
|
v = "'" + v + "'"
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
s.WriteString(" " + v)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
s.WriteString("; ")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return strings.TrimSpace(s.String())
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The default Strict-Transport-Security header. This header tells the browser
|
|
|
|
// The default Strict-Transport-Security header. This header tells the browser
|
|
|
|
// to exclusively use HTTPS for all requests to the origin for the next year.
|
|
|
|
// to exclusively use HTTPS for all requests to the origin for the next year.
|
|
|
@ -130,6 +179,9 @@ type Config struct {
|
|
|
|
// startup.
|
|
|
|
// startup.
|
|
|
|
CSRFSecret []byte
|
|
|
|
CSRFSecret []byte
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// CSP is the Content-Security-Policy header to return with BrowserMux
|
|
|
|
|
|
|
|
// responses.
|
|
|
|
|
|
|
|
CSP CSP
|
|
|
|
// CSPAllowInlineStyles specifies whether to include `style-src:
|
|
|
|
// CSPAllowInlineStyles specifies whether to include `style-src:
|
|
|
|
// unsafe-inline` in the Content-Security-Policy header to permit the use of
|
|
|
|
// unsafe-inline` in the Content-Security-Policy header to permit the use of
|
|
|
|
// inline CSS.
|
|
|
|
// inline CSS.
|
|
|
@ -168,6 +220,10 @@ func (c *Config) setDefaults() error {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if c.CSP == nil {
|
|
|
|
|
|
|
|
c.CSP = DefaultCSP()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -199,16 +255,20 @@ func NewServer(config Config) (*Server, error) {
|
|
|
|
if config.CookiesSameSiteLax {
|
|
|
|
if config.CookiesSameSiteLax {
|
|
|
|
sameSite = csrf.SameSiteLaxMode
|
|
|
|
sameSite = csrf.SameSiteLaxMode
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if config.CSPAllowInlineStyles {
|
|
|
|
|
|
|
|
if _, ok := config.CSP["style-src"]; ok {
|
|
|
|
|
|
|
|
config.CSP.Add("style-src", "unsafe-inline")
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
config.CSP.Set("style-src", "self", "unsafe-inline")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
s := &Server{
|
|
|
|
s := &Server{
|
|
|
|
Config: config,
|
|
|
|
Config: config,
|
|
|
|
csp: defaultCSP,
|
|
|
|
csp: config.CSP.String(),
|
|
|
|
// only set Secure flag on CSRF cookies if we are in a secure context
|
|
|
|
// only set Secure flag on CSRF cookies if we are in a secure context
|
|
|
|
// as otherwise the browser will reject the cookie
|
|
|
|
// as otherwise the browser will reject the cookie
|
|
|
|
csrfProtect: csrf.Protect(config.CSRFSecret, csrf.Secure(config.SecureContext), csrf.SameSite(sameSite)),
|
|
|
|
csrfProtect: csrf.Protect(config.CSRFSecret, csrf.Secure(config.SecureContext), csrf.SameSite(sameSite)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if config.CSPAllowInlineStyles {
|
|
|
|
|
|
|
|
s.csp = defaultCSP + `; style-src 'self' 'unsafe-inline'`
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
s.h = cmp.Or(config.HTTPServer, &http.Server{})
|
|
|
|
s.h = cmp.Or(config.HTTPServer, &http.Server{})
|
|
|
|
if s.h.Handler != nil {
|
|
|
|
if s.h.Handler != nil {
|
|
|
|
return nil, fmt.Errorf("use safeweb.Config.APIMux and safeweb.Config.BrowserMux instead of http.Server.Handler")
|
|
|
|
return nil, fmt.Errorf("use safeweb.Config.APIMux and safeweb.Config.BrowserMux instead of http.Server.Handler")
|
|
|
|