diff --git a/src/socialmediatracker_mcp/auth_manager.py b/src/socialmediatracker_mcp/auth_manager.py index 3e3a99e..efd2a2f 100644 --- a/src/socialmediatracker_mcp/auth_manager.py +++ b/src/socialmediatracker_mcp/auth_manager.py @@ -27,6 +27,46 @@ logger = logging.getLogger(__name__) +def add_security_headers(handler: BaseHTTPRequestHandler, content_type: str = 'text/html'): + """ + Add security headers to HTTP response including Content Security Policy + + Args: + handler: HTTP request handler instance + content_type: Content-Type header value (default: 'text/html') + """ + # Set Content-Type + handler.send_header('Content-Type', content_type) + + # Content Security Policy (CSP) + # Restrictive policy allowing only inline scripts/styles needed for OAuth pages + # - default-src 'none': Deny all by default (defense in depth) + # - script-src 'unsafe-inline': Allow inline scripts (needed for auto-close functionality) + # - style-src 'unsafe-inline': Allow inline styles (pages use embedded CSS) + # - img-src 'self' data:: Allow images from same origin and data URIs + # - font-src 'self': Allow fonts from same origin + # - connect-src 'none': No external connections from pages + # - frame-ancestors 'none': Prevent clickjacking/iframe embedding + # - base-uri 'self': Restrict base tag to same origin + # - form-action 'none': No form submissions from these pages + csp_policy = ( + "default-src 'none'; " + "script-src 'unsafe-inline'; " + "style-src 'unsafe-inline'; " + "img-src 'self' data:; " + "font-src 'self'; " + "connect-src 'none'; " + "frame-ancestors 'none'; " + "base-uri 'self'; " + "form-action 'none'" + ) + handler.send_header('Content-Security-Policy', csp_policy) + + # Additional security headers + handler.send_header('X-Content-Type-Options', 'nosniff') + handler.send_header('X-Frame-Options', 'DENY') + handler.send_header('X-XSS-Protection', '1; mode=block') + class OAuthCallbackHandler(BaseHTTPRequestHandler): """HTTP handler for OAuth callback""" @@ -51,7 +91,7 @@ def do_GET(self): # Return success page self.send_response(200) - self.send_header('Content-type', 'text/html') + add_security_headers(self) self.end_headers() success_html = ''' @@ -133,7 +173,7 @@ def do_GET(self): else: # No code received self.send_response(400) - self.send_header('Content-type', 'text/html') + add_security_headers(self) self.end_headers() self.wfile.write(b'