Files
FunguyBot/plugins/headers.py
T

352 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
This plugin provides comprehensive HTTP security header analysis.
"""
import logging
import aiohttp
import asyncio
import simplematrixbotlib as botlib
from urllib.parse import urlparse
import ssl
import socket
import datetime
from plugins.common import is_public_destination, collapsible_summary, html_escape
async def handle_command(room, message, bot, prefix, config):
"""
Function to handle !headers command for HTTP security header analysis.
"""
match = botlib.MessageMatch(room, message, bot, prefix)
if match.is_not_from_this_bot() and match.prefix() and match.command("headers"):
logging.info("Received !headers command")
args = match.args()
if len(args) < 1:
await show_usage(room, bot)
return
url = args[0].strip()
# Add protocol if missing
if not url.startswith(('http://', 'https://')):
url = 'https://' + url
# SSRF protection: refuse internal hosts
parsed = urlparse(url)
host = parsed.hostname
if not is_public_destination(host):
await bot.api.send_text_message(room.room_id,
"❌ Scanning of private/internal addresses is not allowed.")
return
await analyze_headers(room, bot, url)
async def show_usage(room, bot):
"""Display headers command usage."""
usage = """
<strong>🔒 HTTP Security Headers Analysis</strong>
<strong>!headers &lt;url&gt;</strong> - Comprehensive HTTP security header analysis
<strong>Examples:</strong>
• <code>!headers example.com</code>
• <code>!headers https://github.com</code>
• <code>!headers http://localhost:8080</code>
<strong>Analyzes:</strong>
• Security headers presence and configuration
• SSL/TLS certificate information
• HTTP to HTTPS redirects
• Security scoring and recommendations
"""
await bot.api.send_markdown_message(room.room_id, usage)
async def analyze_headers(room, bot, url):
"""Perform comprehensive HTTP security header analysis."""
try:
await bot.api.send_text_message(room.room_id, f"🔍 Analyzing security headers for: {html_escape(url)}")
results = {
'url': url,
'http_headers': {},
'https_headers': {},
'redirect_chain': [],
'ssl_info': {},
'security_score': 0,
'recommendations': []
}
# Test HTTP first (if HTTPS was provided, we'll still check redirects)
parsed = urlparse(url)
http_url = f"http://{parsed.netloc or parsed.path}"
https_url = f"https://{parsed.netloc or parsed.path}"
# Analyze HTTP response and redirects
await analyze_http_response(results, http_url if not url.startswith('https://') else https_url)
# Analyze HTTPS response
if url.startswith('https://') or results.get('redirects_to_https'):
await analyze_https_response(results, https_url)
# Analyze SSL certificate if HTTPS
if url.startswith('https://') or results.get('redirects_to_https'):
await analyze_ssl_certificate(results, parsed.netloc or parsed.path)
# Calculate security score
await calculate_security_score(results)
# Generate recommendations
await generate_recommendations(results)
# Format and send results
output = await format_header_analysis(results)
await bot.api.send_markdown_message(room.room_id, output)
logging.info(f"Completed header analysis for {url}")
except Exception as e:
await bot.api.send_text_message(room.room_id, f"Error analyzing headers: {str(e)}")
logging.error(f"Error in analyze_headers: {e}")
async def analyze_http_response(results, url):
"""Analyze HTTP response and redirect chain."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, allow_redirects=True, timeout=aiohttp.ClientTimeout(total=10)) as response:
results['final_url'] = str(response.url)
results['status_code'] = response.status
results['http_headers'] = dict(response.headers)
results['redirects_to_https'] = response.url.scheme == 'https'
# aiohttp doesn't give access to redirect history easily, so we'll mark if final URL differs
if str(response.url) != url:
results['redirect_chain'] = [{'url': url, 'status_code': 301}] # simplified
except aiohttp.ClientError as e:
results['http_error'] = str(e)
async def analyze_https_response(results, url):
"""Analyze HTTPS response headers."""
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, allow_redirects=False, timeout=aiohttp.ClientTimeout(total=10)) as response:
results['https_headers'] = dict(response.headers)
results['https_status'] = response.status
except aiohttp.ClientError as e:
results['https_error'] = str(e)
async def analyze_ssl_certificate(results, domain):
"""Analyze SSL certificate information (run in thread to avoid event loop blocking)."""
def _get_cert():
try:
context = ssl.create_default_context()
with socket.create_connection((domain, 443), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=domain) as ssock:
cert = ssock.getpeercert()
return {
'subject': dict(x[0] for x in cert['subject']),
'issuer': dict(x[0] for x in cert['issuer']),
'not_before': cert['notBefore'],
'not_after': cert['notAfter'],
'san': cert.get('subjectAltName', []),
'version': cert.get('version'),
'serial_number': cert.get('serialNumber')
}
except Exception as e:
return f"Error: {e}"
loop = asyncio.get_running_loop()
ssl_data = await loop.run_in_executor(None, _get_cert)
if isinstance(ssl_data, str):
results['ssl_error'] = ssl_data
else:
results['ssl_info'] = ssl_data
async def calculate_security_score(results):
"""Calculate overall security score based on headers and configuration."""
score = 100
missing_headers = []
critical_headers = [
'Strict-Transport-Security',
'Content-Security-Policy',
'X-Content-Type-Options',
'X-Frame-Options',
'X-XSS-Protection'
]
headers = results.get('https_headers') or results.get('http_headers', {})
for header in critical_headers:
if header not in headers:
score -= 15
missing_headers.append(header)
# Check HSTS configuration
hsts = headers.get('Strict-Transport-Security', '')
if 'max-age=31536000' not in hsts:
score -= 10
if 'includeSubDomains' not in hsts:
score -= 5
if 'preload' not in hsts:
score -= 5
# Check CSP configuration
csp = headers.get('Content-Security-Policy', '')
if not csp:
score -= 10
elif "default-src 'none'" not in csp and "default-src 'self'" not in csp:
score -= 5
# Check for insecure headers
insecure_headers = ['Server', 'X-Powered-By', 'X-AspNet-Version']
for header in insecure_headers:
if header in headers:
score -= 5
# Bonus for good practices
if headers.get('Referrer-Policy'):
score += 5
if headers.get('Feature-Policy') or headers.get('Permissions-Policy'):
score += 5
if headers.get('X-Content-Type-Options') == 'nosniff':
score += 5
if headers.get('X-Frame-Options') in ['DENY', 'SAMEORIGIN']:
score += 5
# HTTPS enforcement bonus
if results.get('redirects_to_https'):
score += 10
results['security_score'] = max(0, score)
results['missing_headers'] = missing_headers
async def generate_recommendations(results):
"""Generate security recommendations based on analysis."""
recommendations = []
headers = results.get('https_headers') or results.get('http_headers', {})
if 'Strict-Transport-Security' not in headers:
recommendations.append("🔒 Implement HSTS header with max-age=31536000, includeSubDomains, and preload")
else:
hsts = headers['Strict-Transport-Security']
if 'max-age=31536000' not in hsts:
recommendations.append("🔒 Increase HSTS max-age to 31536000 (1 year)")
if 'includeSubDomains' not in hsts:
recommendations.append("🔒 Add includeSubDomains to HSTS header")
if 'preload' not in hsts:
recommendations.append("🔒 Consider adding preload directive to HSTS for browser preloading")
if 'Content-Security-Policy' not in headers:
recommendations.append("🛡️ Implement Content Security Policy to prevent XSS attacks")
if 'X-Frame-Options' not in headers:
recommendations.append("🚫 Add X-Frame-Options header to prevent clickjacking (DENY or SAMEORIGIN)")
if 'X-Content-Type-Options' not in headers:
recommendations.append("📄 Add X-Content-Type-Options: nosniff to prevent MIME type sniffing")
if 'Referrer-Policy' not in headers:
recommendations.append("🔗 Implement Referrer-Policy to control referrer information leakage")
if 'Server' in headers or 'X-Powered-By' in headers:
recommendations.append("🕵️ Remove Server and X-Powered-By headers to avoid information disclosure")
if not results.get('redirects_to_https') and not results['url'].startswith('https://'):
recommendations.append("🔐 Implement HTTP to HTTPS redirects")
results['recommendations'] = recommendations
async def format_header_analysis(results):
"""Format the header analysis results for display."""
safe_url = html_escape(results['url'])
output = f"<strong>🔒 Security Headers Analysis: {safe_url}</strong><br><br>"
# Security Score
score = results['security_score']
score_emoji = "🟢" if score >= 80 else "🟡" if score >= 60 else "🔴"
output += f"<strong>{score_emoji} Security Score: {score}/100</strong><br><br>"
# Basic Information
output += "<strong>📊 Basic Information</strong><br>"
output += f" • <strong>Final URL:</strong> {html_escape(results.get('final_url', 'N/A'))}<br>"
output += f" • <strong>Status Code:</strong> {results.get('status_code', 'N/A')}<br>"
if results.get('redirects_to_https'):
output += f" • <strong>HTTPS Redirect:</strong> ✅ Enforced<br>"
else:
output += f" • <strong>HTTPS Redirect:</strong> ❌ Not enforced<br>"
output += "<br>"
# Security Headers Analysis
headers = results.get('https_headers') or results.get('http_headers', {})
output += "<strong>🛡️ Security Headers Analysis</strong><br>"
security_headers = {
'Strict-Transport-Security': ('🔒', 'HSTS'),
'Content-Security-Policy': ('🛡️', 'CSP'),
'X-Frame-Options': ('🚫', 'Clickjacking Protection'),
'X-Content-Type-Options': ('📄', 'MIME Sniffing'),
'X-XSS-Protection': ('', 'XSS Protection (Deprecated)'),
'Referrer-Policy': ('🔗', 'Referrer Policy'),
'Feature-Policy': ('⚙️', 'Feature Policy'),
'Permissions-Policy': ('🔧', 'Permissions Policy'),
}
for header, (emoji, description) in security_headers.items():
if header in headers:
value = html_escape(str(headers[header]))[:100]
output += f"{emoji} <strong>{header}:</strong> ✅ {value}<br>"
else:
output += f"{emoji} <strong>{header}:</strong> ❌ Missing<br>"
output += "<br>"
# Other Headers (Information Disclosure)
output += "<strong>📋 Other Headers</strong><br>"
for header in ['Server', 'X-Powered-By']:
if header in headers:
output += f" • 🔍 <strong>{header}:</strong> {html_escape(str(headers[header]))}<br>"
output += "<br>"
# SSL Certificate Information (if available)
if results.get('ssl_info') and 'subject' in results['ssl_info']:
output += "<strong>🔐 SSL Certificate</strong><br>"
ssl_info = results['ssl_info']
if ssl_info.get('subject'):
output += f" • <strong>Subject:</strong> {html_escape(ssl_info['subject'].get('commonName', 'N/A'))}<br>"
if ssl_info.get('issuer'):
output += f" • <strong>Issuer:</strong> {html_escape(ssl_info['issuer'].get('organizationName', 'N/A'))}<br>"
if ssl_info.get('not_after'):
output += f" • <strong>Expires:</strong> {html_escape(ssl_info['not_after'])}<br>"
output += "<br>"
# Recommendations
if results.get('recommendations'):
output += "<strong>💡 Security Recommendations</strong><br>"
for rec in results['recommendations'][:8]:
output += f"{rec}<br>"
output += "<br>"
# Final rating
if score >= 80:
rating = "🟢 Excellent"
elif score >= 60:
rating = "🟡 Good"
elif score >= 40:
rating = "🟠 Fair"
else:
rating = "🔴 Poor"
output += f"<strong>📈 Security Rating:</strong> {rating}<br>"
# Wrap in collapsible details
return collapsible_summary(f"🔒 Security Headers Analysis: {safe_url} (Score: {score}/100)", output)
__version__ = "1.0.2"
__author__ = "Funguy Bot"
__description__ = "HTTP security header analysis (SSRFsafe, async)"
__help__ = """
<details>
<summary><strong>!headers</strong> HTTP security header scanner</summary>
<p><code>!headers &lt;url&gt;</code> Checks HSTS, CSP, X-Frame-Options, etc.<br>
Provides security score (0-100) and recommendations. Also shows SSL certificate info.</p>
</details>
"""