alert-manager - Add strict CSP support.
See https://csp.withgoogle.com/docs/strict-csp.html for details
on strict CSP.
Bug: skia:
Change-Id: I6cd5243d47688b77263349ac971b270dc51241c6
Reviewed-on: https://skia-review.googlesource.com/c/180220
Reviewed-by: Ravi Mistry <rmistry@google.com>
Commit-Queue: Joe Gregorio <jcgregorio@google.com>
diff --git a/am/Makefile b/am/Makefile
index 4b23b42..6a938e9 100644
--- a/am/Makefile
+++ b/am/Makefile
@@ -11,7 +11,7 @@
./build_alert_to_pubsub_release
debug:
- npx webpack --mode=production
+ npx webpack --mode=development
legacy_release: build package-lock.json
./build_legacy_alert_to_pubsub_release "`git log -n1 --format=%s`"
diff --git a/am/go/alert-manager/main.go b/am/go/alert-manager/main.go
index 5e84cc3..7a4efc7 100644
--- a/am/go/alert-manager/main.go
+++ b/am/go/alert-manager/main.go
@@ -16,10 +16,9 @@
"time"
"cloud.google.com/go/pubsub"
- "github.com/99designs/goodies/http/secure_headers/csp"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
- "github.com/unrolled/secure"
+ "github.com/skia-dev/secure"
"go.skia.org/infra/am/go/incident"
"go.skia.org/infra/am/go/note"
"go.skia.org/infra/am/go/silence"
@@ -235,6 +234,8 @@
if err := srv.templates.ExecuteTemplate(w, "index.html", map[string]string{
// base64 encode the csrf to avoid golang templating escaping.
"csrf": base64.StdEncoding.EncodeToString([]byte(csrf.Token(r))),
+ // Look in webpack.config.js for where the nonce templates are injected.
+ "nonce": secure.CSPNonce(r.Context()),
}); err != nil {
sklog.Errorf("Failed to expand template: %s", err)
}
@@ -586,20 +587,51 @@
}
}
+// cspReportWrapper wraps a handler and intercepts csp failure reports and
+// turns them into structured log entries.
+//
+// Note this should be outside the csrf wrapper since execution may fail
+// before the csrf is in place.
+func cspReportWrapper(h http.Handler) http.Handler {
+ s := func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/cspreport" && r.Method == "POST" {
+ var body interface{}
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+ sklog.Errorf("Failed to decode csp report: %s", err)
+ return
+ }
+ c := struct {
+ Type string `json:"type"`
+ Body interface{} `json:"body"`
+ }{
+ Type: "csp",
+ Body: body,
+ }
+ b, err := json.Marshal(c)
+ if err != nil {
+ sklog.Errorf("Failed to marshal csp log entry: %s", err)
+ return
+ }
+ fmt.Println(string(b))
+ return
+ } else {
+ h.ServeHTTP(w, r)
+ }
+ }
+ return http.HandlerFunc(s)
+}
+
func (srv *Server) applySecurityWrappers(h http.Handler) http.Handler {
// Configure Content Security Policy (CSP).
- cspOpts := csp.Opts{
- DefaultSrc: []string{csp.SourceNone},
- ConnectSrc: []string{"https://skia.org", "https://skia-tree-status.appspot.com", csp.SourceSelf},
- ImgSrc: []string{csp.SourceSelf},
- StyleSrc: []string{csp.SourceSelf, csp.SourceUnsafeInline},
- ScriptSrc: []string{csp.SourceSelf},
- }
-
+ addScriptSrc := ""
if *local {
// webpack uses eval() in development mode, so allow unsafe-eval when local.
- cspOpts.ScriptSrc = append(cspOpts.ScriptSrc, "'unsafe-eval'")
+ addScriptSrc = "'unsafe-eval'"
}
+ // This non-local CSP string passes the tests at https://csp-evaluator.withgoogle.com/.
+ //
+ // See also: https://csp.withgoogle.com/docs/strict-csp.html
+ cspString := fmt.Sprintf("base-uri 'none'; img-src 'self' ; object-src 'none' ; style-src 'self' 'unsafe-inline' ; script-src 'strict-dynamic' $NONCE 'unsafe-inline' %s https: http: ; report-uri /cspreport ;", addScriptSrc)
// Apply CSP and other security minded headers.
secureMiddleware := secure.New(secure.Options{
@@ -609,7 +641,7 @@
SSLProxyHeaders: map[string]string{"X-Forwarded-Proto": "https"},
STSSeconds: 60 * 60 * 24 * 365,
STSIncludeSubdomains: true,
- ContentSecurityPolicy: cspOpts.Header(),
+ ContentSecurityPolicy: cspString,
IsDevelopment: *local,
})
@@ -670,6 +702,7 @@
h = login.ForceAuth(h, login.DEFAULT_REDIRECT_URL)
h = httputils.HealthzAndHTTPS(h)
}
+ h = cspReportWrapper(h)
http.Handle("/", h)
sklog.Infoln("Ready to serve.")
sklog.Fatal(http.ListenAndServe(*port, nil))
diff --git a/am/package.json b/am/package.json
index 9fb5cd9..f1fd543 100644
--- a/am/package.json
+++ b/am/package.json
@@ -11,13 +11,15 @@
"dependencies": {
"@webcomponents/custom-elements": "~1.2.1",
"common-sk": "~3.1.0",
- "infra-sk": "~0.8.1",
"elements-sk": "~2.7.0",
+ "html-webpack-inject-attributes-plugin": "^1.0.1",
+ "infra-sk": "~0.8.1",
"lit-html": "~0.14.0"
},
"devDependencies": {
"chai": "~4.2.0",
"copy-webpack-plugin": "~4.6.0",
+ "html-webpack-plugin": "^3.2.0",
"karma": "~3.1.4",
"karma-chai": "~0.1.0",
"karma-chrome-launcher": "~2.2.0",
diff --git a/am/webpack.config.js b/am/webpack.config.js
index eb6174c..39e1f40 100644
--- a/am/webpack.config.js
+++ b/am/webpack.config.js
@@ -1,5 +1,6 @@
const commonBuilder = require('pulito');
-const CopyWebpackPlugin = require('copy-webpack-plugin')
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const HtmlWebpackInjectAttributesPlugin = require('html-webpack-inject-attributes-plugin');
module.exports = (env, argv) => {
let config = commonBuilder(env, argv, __dirname);
@@ -17,5 +18,10 @@
}
])
);
+ config.plugins.push(
+ new HtmlWebpackInjectAttributesPlugin({
+ nonce: "{%.nonce%}",
+ }),
+ );
return config;
}