From 6697d8890ba056b660ff8ca5bcf52db64736c08e Mon Sep 17 00:00:00 2001 From: pac7 <47831526-pac7@users.noreply.replit.com> Date: Mon, 22 Sep 2025 16:06:47 +0000 Subject: [PATCH] Enhance website security and add SEO meta tags for better visibility Implement robust security headers, including CSP with nonces, and integrate comprehensive SEO meta tags into the base template and homepage. Add inline styles for CSP compliance and improve theme management script for immediate theme application. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 48ecdb60-d0f0-4b75-95c9-34e409ef35fb Replit-Commit-Checkpoint-Type: intermediate_checkpoint --- .replit | 4 - apps/core/middleware/security_headers.py | 97 ++++++++++++++++ config/django/base.py | 1 + config/settings/security.py | 34 ++++++ static/css/inline-styles.css | 29 +++++ static/css/tailwind.css | 5 + static/js/theme.js | 16 +++ templates/base/base.html | 140 +++++++++++++++-------- templates/home.html | 41 +++++++ 9 files changed, 315 insertions(+), 52 deletions(-) create mode 100644 apps/core/middleware/security_headers.py create mode 100644 static/css/inline-styles.css create mode 100644 static/js/theme.js diff --git a/.replit b/.replit index 8f36374f..3520ba63 100644 --- a/.replit +++ b/.replit @@ -62,10 +62,6 @@ externalPort = 3000 localPort = 45245 externalPort = 3001 -[[ports]] -localPort = 46739 -externalPort = 3002 - [deployment] deploymentTarget = "autoscale" run = [ diff --git a/apps/core/middleware/security_headers.py b/apps/core/middleware/security_headers.py new file mode 100644 index 00000000..67d74e92 --- /dev/null +++ b/apps/core/middleware/security_headers.py @@ -0,0 +1,97 @@ +""" +Modern Security Headers Middleware for ThrillWiki +Implements Content Security Policy and other modern security headers. +""" + +import secrets +import base64 +from django.conf import settings +from django.utils.deprecation import MiddlewareMixin + + +class SecurityHeadersMiddleware(MiddlewareMixin): + """ + Middleware to add modern security headers to all responses. + """ + + def _generate_nonce(self): + """Generate a cryptographically secure nonce for CSP.""" + # Generate 16 random bytes and encode as base64 + return base64.b64encode(secrets.token_bytes(16)).decode('ascii') + + def _modify_csp_with_nonce(self, csp_policy, nonce): + """Modify CSP policy to include nonce for script-src.""" + if not csp_policy: + return csp_policy + + # Look for script-src directive and add nonce + directives = csp_policy.split(';') + modified_directives = [] + + for directive in directives: + directive = directive.strip() + if directive.startswith('script-src '): + # Add nonce to script-src directive + directive += f" 'nonce-{nonce}'" + modified_directives.append(directive) + + return '; '.join(modified_directives) + + def process_request(self, request): + """Generate and store nonce for this request.""" + # Generate a nonce for this request + nonce = self._generate_nonce() + # Store it in request so templates can access it + request.csp_nonce = nonce + return None + + def process_response(self, request, response): + """Add security headers to the response.""" + + # Content Security Policy with nonce support + if hasattr(settings, 'SECURE_CONTENT_SECURITY_POLICY'): + csp_policy = settings.SECURE_CONTENT_SECURITY_POLICY + # Apply nonce if we have one for this request + if hasattr(request, 'csp_nonce'): + csp_policy = self._modify_csp_with_nonce(csp_policy, request.csp_nonce) + response['Content-Security-Policy'] = csp_policy + + # Cross-Origin Opener Policy + if hasattr(settings, 'SECURE_CROSS_ORIGIN_OPENER_POLICY'): + response['Cross-Origin-Opener-Policy'] = settings.SECURE_CROSS_ORIGIN_OPENER_POLICY + + # Referrer Policy + if hasattr(settings, 'SECURE_REFERRER_POLICY'): + response['Referrer-Policy'] = settings.SECURE_REFERRER_POLICY + + # Permissions Policy + if hasattr(settings, 'SECURE_PERMISSIONS_POLICY'): + response['Permissions-Policy'] = settings.SECURE_PERMISSIONS_POLICY + + # Additional security headers + response['X-Content-Type-Options'] = 'nosniff' + response['X-Frame-Options'] = getattr(settings, 'X_FRAME_OPTIONS', 'DENY') + response['X-XSS-Protection'] = '1; mode=block' + + # Cache Control headers for better performance + # Prevent caching of HTML pages to ensure users get fresh content + if response.get('Content-Type', '').startswith('text/html'): + response['Cache-Control'] = 'no-cache, no-store, must-revalidate' + response['Pragma'] = 'no-cache' + response['Expires'] = '0' + + # Strict Transport Security (if SSL is enabled) + if getattr(settings, 'SECURE_SSL_REDIRECT', False): + hsts_seconds = getattr(settings, 'SECURE_HSTS_SECONDS', 31536000) + hsts_include_subdomains = getattr(settings, 'SECURE_HSTS_INCLUDE_SUBDOMAINS', True) + hsts_preload = getattr(settings, 'SECURE_HSTS_PRELOAD', False) + + hsts_header = f'max-age={hsts_seconds}' + if hsts_include_subdomains: + hsts_header += '; includeSubDomains' + if hsts_preload: + hsts_header += '; preload' + + response['Strict-Transport-Security'] = hsts_header + + return response \ No newline at end of file diff --git a/config/django/base.py b/config/django/base.py index 13a385d8..3bb5bee2 100644 --- a/config/django/base.py +++ b/config/django/base.py @@ -122,6 +122,7 @@ MIDDLEWARE = [ "django.middleware.cache.UpdateCacheMiddleware", "corsheaders.middleware.CorsMiddleware", # CORS middleware for API "django.middleware.security.SecurityMiddleware", + "apps.core.middleware.security_headers.SecurityHeadersMiddleware", # Modern security headers "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", diff --git a/config/settings/security.py b/config/settings/security.py index 32586aa2..28d8374c 100644 --- a/config/settings/security.py +++ b/config/settings/security.py @@ -34,3 +34,37 @@ SESSION_COOKIE_SAMESITE = env("SESSION_COOKIE_SAMESITE", default="Lax") CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=False) CSRF_COOKIE_HTTPONLY = env.bool("CSRF_COOKIE_HTTPONLY", default=True) CSRF_COOKIE_SAMESITE = env("CSRF_COOKIE_SAMESITE", default="Lax") + +# Content Security Policy (CSP) - Tightened security without unsafe directives +SECURE_CONTENT_SECURITY_POLICY = env( + "SECURE_CONTENT_SECURITY_POLICY", + default=( + "default-src 'self'; " + "script-src 'self' " + "https://unpkg.com https://cdnjs.cloudflare.com; " + "style-src 'self' " + "https://fonts.googleapis.com https://cdnjs.cloudflare.com; " + "img-src 'self' data: https: blob:; " + "font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com; " + "connect-src 'self'; " + "media-src 'self'; " + "object-src 'none'; " + "frame-src 'none'; " + "worker-src 'self'; " + "manifest-src 'self'; " + "base-uri 'self'; " + "form-action 'self'; " + "upgrade-insecure-requests;" + ) +) + +# Additional modern security headers +SECURE_CROSS_ORIGIN_OPENER_POLICY = env("SECURE_CROSS_ORIGIN_OPENER_POLICY", default="same-origin") +SECURE_REFERRER_POLICY = env("SECURE_REFERRER_POLICY", default="strict-origin-when-cross-origin") +SECURE_PERMISSIONS_POLICY = env( + "SECURE_PERMISSIONS_POLICY", + default="geolocation=(), camera=(), microphone=(), payment=()" +) + +# X-Frame-Options alternative - more flexible +X_FRAME_OPTIONS = env("X_FRAME_OPTIONS", default="DENY") diff --git a/static/css/inline-styles.css b/static/css/inline-styles.css new file mode 100644 index 00000000..502b3652 --- /dev/null +++ b/static/css/inline-styles.css @@ -0,0 +1,29 @@ +/* Inline styles that were moved from the base template for CSP compliance */ + +[x-cloak] { + display: none !important; +} + +.dropdown-menu { + position: absolute; + right: 0; + margin-top: 0.5rem; + width: 12rem; + border-radius: 0.375rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05); + z-index: 50; + overflow: hidden; +} + +.htmx-indicator { + display: none; +} + +.htmx-request .htmx-indicator { + display: block; +} + +.htmx-request.htmx-indicator { + display: block; +} \ No newline at end of file diff --git a/static/css/tailwind.css b/static/css/tailwind.css index 2e75e5d1..a14b6601 100644 --- a/static/css/tailwind.css +++ b/static/css/tailwind.css @@ -2957,6 +2957,11 @@ left: calc(var(--spacing) * 4); } } + .focus\:z-50 { + &:focus { + z-index: 50; + } + } .focus\:border-blue-500 { &:focus { border-color: var(--color-blue-500); diff --git a/static/js/theme.js b/static/js/theme.js new file mode 100644 index 00000000..44fa008b --- /dev/null +++ b/static/js/theme.js @@ -0,0 +1,16 @@ +/** + * Theme management script + * Prevents flash of wrong theme by setting theme class immediately + */ +(function() { + let theme = localStorage.getItem("theme"); + if (!theme) { + theme = window.matchMedia("(prefers-color-scheme: dark)").matches + ? "dark" + : "light"; + localStorage.setItem("theme", theme); + } + if (theme === "dark") { + document.documentElement.classList.add("dark"); + } +})(); \ No newline at end of file diff --git a/templates/base/base.html b/templates/base/base.html index 95951dca..f88b1251 100644 --- a/templates/base/base.html +++ b/templates/base/base.html @@ -5,30 +5,68 @@ + +