From 6114111654fba28e3f460d3c9463ac2b10a92343 Mon Sep 17 00:00:00 2001 From: Aevann Date: Tue, 27 Dec 2022 03:22:39 +0200 Subject: [PATCH] tighten CSP --- files/assets/js/service_worker.js | 2 +- files/routes/allroutes.py | 78 ++++++++++++++++---- files/routes/static.py | 8 ++ files/routes/wrappers.py | 8 +- files/templates/admin/alts.html | 2 +- files/templates/admin/badge_admin.html | 2 +- files/templates/admin/banned_domains.html | 2 +- files/templates/awards.html | 2 +- files/templates/casino/blackjack_screen.html | 4 +- files/templates/casino/game_screen.html | 6 +- files/templates/casino/roulette_screen.html | 4 +- files/templates/casino/slots_screen.html | 4 +- files/templates/chat.html | 2 +- files/templates/comments.html | 8 +- files/templates/default.html | 2 +- files/templates/email/default.html | 2 - files/templates/errors/WPD/502.html | 36 +-------- files/templates/errors/rDrama/502.html | 36 +-------- files/templates/event/awards.html | 14 ++-- files/templates/event/banner.html | 2 +- files/templates/event/banner.svg | 2 +- files/templates/event/banner_rDrama.svg | 2 +- files/templates/event/music.html | 2 +- files/templates/hats.html | 4 +- files/templates/header.html | 12 +-- files/templates/home.html | 12 +-- files/templates/log.html | 8 +- files/templates/login/login_2fa.html | 2 +- files/templates/modals/emoji.html | 4 +- files/templates/notifications.html | 2 +- files/templates/settings/css.html | 2 +- files/templates/settings/personal.html | 6 +- files/templates/shop.html | 2 +- files/templates/sub/mods.html | 2 +- files/templates/submission.html | 13 ++-- files/templates/submission_banned.html | 2 +- files/templates/submission_listing.html | 2 +- files/templates/submit.html | 2 +- files/templates/submit_hats.html | 2 +- files/templates/submit_marseys.html | 4 +- files/templates/update_assets.html | 2 +- files/templates/userpage/userpage.html | 2 +- files/templates/util/html_head.html | 16 ++-- nginx-headers.conf | 1 - nginx-serve-static.conf | 1 + nginx.conf | 12 +++ 46 files changed, 182 insertions(+), 163 deletions(-) diff --git a/files/assets/js/service_worker.js b/files/assets/js/service_worker.js index 8aac6de88..347e21deb 100644 --- a/files/assets/js/service_worker.js +++ b/files/assets/js/service_worker.js @@ -1,7 +1,7 @@ 'use strict'; const CACHE_NAME = "offlineCache-v1"; -const OFFLINE_URL = "/assets/offline.html"; +const OFFLINE_URL = "/offline.html"; self.addEventListener("install", () => { const cacheOfflinePage = async () => { diff --git a/files/routes/allroutes.py b/files/routes/allroutes.py index fb32f6c0d..5dfad83ff 100644 --- a/files/routes/allroutes.py +++ b/files/routes/allroutes.py @@ -13,26 +13,30 @@ def session_init(): @app.before_request def before_request(): - g.desires_auth = False - if not IS_LOCALHOST: - app.config["COOKIE_DOMAIN"] = f".{request.host}" - app.config["SESSION_COOKIE_DOMAIN"] = app.config["COOKIE_DOMAIN"] - if SITE == 'marsey.world' and request.path != '/kofi': - abort(404) - - g.agent = request.headers.get("User-Agent", "") - if not g.agent and request.path != '/kofi': - return 'Please use a "User-Agent" header!', 403 - - ua = g.agent.lower() - if request.host != SITE: abort(403, "Unauthorized host provided!") + if SITE == 'marsey.world' and request.path != '/kofi': + abort(404) + if request.headers.get("CF-Worker"): abort(403, "Cloudflare workers are not allowed to access this website.") - if not get_setting('bots') and request.headers.get("Authorization"): abort(403) + g.agent = request.headers.get("User-Agent", "") + if not g.agent and request.path != '/kofi': + abort(403, 'Please use a "User-Agent" header!') + + if not get_setting('bots') and request.headers.get("Authorization"): + abort(403) + + g.desires_auth = False + if not IS_LOCALHOST: + app.config["COOKIE_DOMAIN"] = f".{request.host}" + app.config["SESSION_COOKIE_DOMAIN"] = app.config["COOKIE_DOMAIN"] + + ua = g.agent.lower() + + g.nonce = None if '; wv) ' in ua: g.browser = 'webview' @@ -54,11 +58,57 @@ def before_request(): limiter.check() g.db = db_session() + + +CSP = { + "upgrade-insecure-requests": "", + + "default-src": "'none'", + "frame-ancestors": "'none'", + + "form-action": "'self'", + "manifest-src": "'self'", + "worker-src": "'self'", + "base-uri": "'self'", + "font-src": "'self'", + "media-src": "'self'", + + "style-src-elem": "'self' 'nonce-{nonce}'", + "style-src-attr": "'unsafe-inline'", + "style-src": "'self' 'unsafe-inline'", + + "script-src-elem": "'self' 'nonce-{nonce}' challenges.cloudflare.com", + "script-src-attr": "'unsafe-inline'", + "script-src": "'self' 'unsafe-inline' challenges.cloudflare.com", + + "img-src": "https:", + "frame-src": "challenges.cloudflare.com www.youtube-nocookie.com platform.twitter.com", + "connect-src": "'self' tls-use1.fpapi.io api.fpjs.io", + + "report-to": "csp", + "report-uri": "/csp_violations", +} + +if IS_LOCALHOST: + CSP["style-src-elem"] += " rdrama.net" + CSP["script-src-elem"] += " rdrama.net" + CSP["img-src"] += " http:" + +CSP_str = '' + +for k, val in CSP.items(): + CSP_str += f'{k} {val}; ' + @app.after_request def after_request(response:Response): if response.status_code < 400: _set_cloudflare_cookie(response) _commit_and_close_db() + + if g.nonce: + response.headers.add("Report-To", {"group":"csp","max_age":10886400,"endpoints":[{"url":"/csp_violations"}]}) + response.headers.add("Content-Security-Policy", CSP_str.format(nonce=g.nonce)) + return response diff --git a/files/routes/static.py b/files/routes/static.py index 07643d21f..b8cdf13d9 100644 --- a/files/routes/static.py +++ b/files/routes/static.py @@ -333,3 +333,11 @@ if not os.path.exists(f'files/templates/donate_{SITE_NAME}.html'): @auth_desired_with_logingate def donate(v): return render_template(f'donate_{SITE_NAME}.html', v=v) + + +@app.post('/csp_violations') +@limiter.limit("10/minute;50/day") +def csp_violations(): + content = request.get_json(force=True) + print(json.dumps(content, indent=4, sort_keys=True), flush=True) + return '' diff --git a/files/routes/wrappers.py b/files/routes/wrappers.py index 29f154f47..d3fccb921 100644 --- a/files/routes/wrappers.py +++ b/files/routes/wrappers.py @@ -1,4 +1,5 @@ import time +import secrets from flask import g, request, session from files.classes.clients import ClientAuth @@ -40,7 +41,8 @@ def get_logged_in_user(): else: session.pop("lo_user") - g.is_api_or_xhr = bool((v and v.client) or request.headers.get("xhr")) + g.is_api = v and v.client + g.is_api_or_xhr = bool(g.is_api or request.headers.get("xhr")) if request.method.lower() != "get" and get_setting('read_only_mode') and not (v and v.admin_level >= PERMS['SITE_BYPASS_READ_ONLY_MODE']): abort(403) @@ -67,6 +69,10 @@ def get_logged_in_user(): if f'@{v.username}, ' not in f.read(): t = time.strftime("%d/%B/%Y %H:%M:%S UTC", time.gmtime(time.time())) log_file(f'@{v.username}, {v.truescore}, {ip}, {t}\n', 'eg.log') + + if not g.is_api: + g.nonce = secrets.token_urlsafe(16) + return v def auth_desired(f): diff --git a/files/templates/admin/alts.html b/files/templates/admin/alts.html index a07f21dca..87d8a1a2c 100644 --- a/files/templates/admin/alts.html +++ b/files/templates/admin/alts.html @@ -66,7 +66,7 @@ {% endif %} - + {% if IS_LOCALHOST %} {% else %} diff --git a/files/templates/comments.html b/files/templates/comments.html index ef1cfbaa2..f37eadde1 100644 --- a/files/templates/comments.html +++ b/files/templates/comments.html @@ -1,7 +1,7 @@ {%- import 'util/macros.html' as macros with context -%} {% if not request.headers.get("xhr") %} {% if comment_info %} - @@ -246,7 +246,7 @@ {% endif %} {% if c.award_count("tilt", v) %} - @@ -50,7 +23,6 @@ - @@ -93,7 +65,7 @@