diff --git a/webapp/handlers.py b/webapp/handlers.py index 00e9ef063f..3b3f147928 100644 --- a/webapp/handlers.py +++ b/webapp/handlers.py @@ -30,6 +30,97 @@ ) from webapp.shop.flaskparser import UAContractsValidationError +CSP = { + "default-src": ["'self'"], + "img-src": [ + "data: blob:", + # This is needed to allow images from + # https://www.google.*/ads/ga-audiences to load. + "*", + ], + "script-src-elem": [ + "'self'", + "assets.ubuntu.com", + "www.google-analytics.com", + "www.googletagmanager.com", + "dev.visualwebsiteoptimizer.com", + "www.youtube.com", + "asciinema.org", + "player.vimeo.com", + "script.crazyegg.com", + "w.usabilla.com", + "munchkin.marketo.net", + "serve.nrich.ai", + "ml314.com", + "scout-cdn.salesloft.com", + "snippet.maze.co", + "www.googleadservices.com", + "js.zi-scripts.com", + "*.g.doubleclick.net", + "www.google.com", + "www.gstatic.com", + "*.googlesyndication.com", + "js.stripe.com", + "d3js.org", + "www.brighttalk.com", + "cdnjs.cloudflare.com", + "static.ads-twitter.com", + "*.cdn.digitaloceanspaces.com", + "www.redditstatic.com", + "snap.licdn.com", + "connect.facebook.net", + # This is necessary for Google Tag Manager to function properly. + "'unsafe-inline'", + ], + "font-src": [ + "'self'", + "assets.ubuntu.com", + ], + "script-src": [ + "'self'", + "blob:", + "'unsafe-eval'", + "'unsafe-hashes'", + "'unsafe-inline'", + ], + "connect-src": [ + "'self'", + "*.googlesyndication.com", + "www.google.com", + "ubuntu.com", + "analytics.google.com", + "www.googletagmanager.com", + "sentry.is.canonical.com", + "www.google-analytics.com", + "*.crazyegg.com", + "scout.salesloft.com", + "*.g.doubleclick.net", + "js.zi-scripts.com", + "*.mktoresp.com", + "prompts.maze.co", + "*.google-analytics.com", + ], + "frame-src": [ + "'self'", + "*.doubleclick.net", + "www.youtube.com/", + "asciinema.org", + "player.vimeo.com", + "js.stripe.com", + "www.googletagmanager.com", + "www.google.com", + "www.brighttalk.com", + ], + "style-src": [ + "'self'", + "'unsafe-inline'", + ], + "media-src": [ + "'self'", + "res.cloudinary.com", + ], +} + def init_handlers(app, sentry): @app.after_request @@ -189,6 +280,42 @@ def context(): def utility_processor(): return {"image": image_template} + @app.after_request + def add_headers(response): + """ + Generic rules for headers to add to all requests + - Content-Security-Policy: Restrict resources (e.g., JavaScript, CSS, + Images) and URLs + - Referrer-Policy: Limit referrer data for security while preserving + full referrer for same-origin requests + - Cross-Origin-Embedder-Policy: allows embedding cross-origin + resources + - Cross-Origin-Opener-Policy: enable the page to open pop-ups while + maintaining same-origin policy + - Cross-Origin-Resource-Policy: allowing cross-origin requests to + access the resource + - X-Permitted-Cross-Domain-Policies: disallows cross-domain access to + resources. + """ + + def get_csp_as_str(csp={}): + csp_str = "" + for key, values in csp.items(): + csp_value = " ".join(values) + csp_str += f"{key} {csp_value}; " + return csp_str.strip() + + response.headers["Content-Security-Policy"] = get_csp_as_str(CSP) + + response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin" + response.headers["Cross-Origin-Embedder-Policy"] = "unsafe-none" + response.headers["Cross-Origin-Opener-Policy"] = ( + "same-origin-allow-popups" + ) + response.headers["Cross-Origin-Resource-Policy"] = "cross-origin" + response.headers["X-Permitted-Cross-Domain-Policies"] = "none" + return response + app.add_template_filter(date_has_passed) app.add_template_filter(sort_by_key_and_ordered_list)