Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 46 additions & 6 deletions src/socialmediatracker_mcp/auth_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand All @@ -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 = '''
Expand Down Expand Up @@ -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'<h1>Error: No authorization code received</h1>')
else:
Expand All @@ -159,7 +199,7 @@ def do_GET(self):
if parsed_path.path == '/auth-choice':
# Display authentication choice page
self.send_response(200)
self.send_header('Content-type', 'text/html')
add_security_headers(self)
self.end_headers()

choice_html = '''
Expand Down Expand Up @@ -309,7 +349,7 @@ def do_GET(self):

# Return loading page
self.send_response(200)
self.send_header('Content-type', 'text/html')
add_security_headers(self)
self.end_headers()

loading_html = '''
Expand Down Expand Up @@ -378,7 +418,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 = '''
Expand Down Expand Up @@ -459,7 +499,7 @@ def do_GET(self):
self.wfile.write(success_html.encode())
else:
self.send_response(400)
self.send_header('Content-type', 'text/html')
add_security_headers(self)
self.end_headers()
self.wfile.write(b'<h1>Error: No authorization code received</h1>')
else:
Expand Down