Repository navigation
Expand file tree
/
Copy pathrek_headers_audit.py
More file actions
222 lines (192 loc) · 9.07 KB
/
Copy pathrek_headers_audit.py
File metadata and controls
222 lines (192 loc) · 9.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
"""
REK Headers Audit - CORS misconfiguration and security headers analysis
Checks for: CORS wildcards, missing security headers, exposed server info, etc.
"""
import asyncio
import httpx
import csv
import os
import json
from typing import List, Dict, Optional
from termcolor import colored
import logging
logger = logging.getLogger(__name__)
# Security headers we expect to see
SECURITY_HEADERS = {
'x-frame-options': 'Missing X-Frame-Options (Clickjacking risk)',
'x-content-type-options': 'Missing X-Content-Type-Options',
'x-xss-protection': 'Missing X-XSS-Protection',
'content-security-policy': 'Missing Content-Security-Policy',
'strict-transport-security': 'Missing HSTS',
'referrer-policy': 'Missing Referrer-Policy',
'permissions-policy': 'Missing Permissions-Policy',
}
# Headers that shouldn't be exposed
SENSITIVE_HEADERS = [
'server', 'x-powered-by', 'x-aspnet-version', 'x-aspnetmvc-version',
'x-drupal-cache', 'x-generator', 'x-wp-nonce',
]
CORS_REFLECT_ORIGINS = [
'https://evil.com',
'https://attacker.com',
'null',
]
class HeadersAuditor:
def __init__(self, timeout: int = 10, concurrency: int = 30, silent: bool = False):
self.timeout = timeout
self.concurrency = concurrency
self.silent = silent
self.findings: List[Dict] = []
def analyze_cors(self, url: str, headers: dict, reflected_origin: str) -> List[Dict]:
"""Analyze CORS response headers for misconfigurations."""
issues = []
acao = headers.get('access-control-allow-origin', '')
acac = headers.get('access-control-allow-credentials', '').lower()
if acao == '*':
issues.append({
'url': url, 'category': 'CORS', 'severity': 'Medium',
'issue': 'Wildcard CORS origin (*)',
'header': 'Access-Control-Allow-Origin: *',
'detail': 'Any origin can read responses. Dangerous with credentials.',
})
elif acao and reflected_origin in acao:
if acac == 'true':
issues.append({
'url': url, 'category': 'CORS', 'severity': 'High',
'issue': 'CORS Origin Reflected + Credentials Allowed',
'header': f'ACAO: {acao}, ACAC: {acac}',
'detail': f'Origin {reflected_origin} is reflected AND credentials are allowed. Critical CORS misconfiguration.',
})
else:
issues.append({
'url': url, 'category': 'CORS', 'severity': 'Medium',
'issue': 'CORS Origin Reflected without credentials',
'header': f'ACAO: {acao}',
'detail': f'Origin {reflected_origin} is reflected. May allow cross-origin reads.',
})
# Null origin
if acao == 'null':
issues.append({
'url': url, 'category': 'CORS', 'severity': 'High',
'issue': 'CORS null origin allowed',
'header': 'Access-Control-Allow-Origin: null',
'detail': 'null origin can be triggered from sandboxed iframes.',
})
return issues
def analyze_security_headers(self, url: str, headers: dict) -> List[Dict]:
"""Check for missing or misconfigured security headers."""
issues = []
lower_headers = {k.lower(): v for k, v in headers.items()}
for header, message in SECURITY_HEADERS.items():
if header not in lower_headers:
severity = 'High' if header in ('strict-transport-security', 'content-security-policy') else 'Low'
issues.append({
'url': url, 'category': 'Security_Headers', 'severity': severity,
'issue': message, 'header': header, 'detail': f'Header {header} is not set.',
})
for header in SENSITIVE_HEADERS:
if header in lower_headers:
issues.append({
'url': url, 'category': 'Info_Disclosure', 'severity': 'Info',
'issue': f'Exposed {header} header',
'header': f'{header}: {lower_headers[header]}',
'detail': f'Technology fingerprinting: {lower_headers[header]}',
})
# Check HSTS details if present
hsts = lower_headers.get('strict-transport-security', '')
if hsts and 'max-age' in hsts:
try:
max_age = int(re.search(r'max-age=(\d+)', hsts).group(1))
if max_age < 31536000:
issues.append({
'url': url, 'category': 'Security_Headers', 'severity': 'Low',
'issue': 'HSTS max-age too short (< 1 year)',
'header': f'Strict-Transport-Security: {hsts}',
'detail': f'max-age={max_age}. Recommend >= 31536000.',
})
except Exception:
pass
return issues
async def audit_url(self, client: httpx.AsyncClient, url: str, semaphore: asyncio.Semaphore) -> List[Dict]:
"""Audit a single URL for CORS and security header issues."""
async with semaphore:
url_issues = []
try:
# Normal request
r = await client.get(url, timeout=self.timeout, follow_redirects=True)
headers = dict(r.headers)
url_issues.extend(self.analyze_security_headers(url, headers))
# CORS probe with evil origins
for origin in CORS_REFLECT_ORIGINS:
try:
r2 = await client.get(
url,
headers={'Origin': origin},
timeout=self.timeout,
follow_redirects=True
)
cors_issues = self.analyze_cors(url, dict(r2.headers), origin)
url_issues.extend(cors_issues)
if cors_issues:
break # Found an issue, no need to test more origins
except Exception:
pass
except Exception as e:
pass
for issue in url_issues:
sev_color = {'High': 'red', 'Medium': 'yellow', 'Low': 'cyan', 'Info': 'white'}.get(issue.get('severity', 'Info'), 'white')
if not self.silent:
print(colored(f"[{issue['severity']}] {issue['issue']} @ {url}", sev_color))
return url_issues
async def audit_all(self, urls: List[str]) -> List[Dict]:
"""Audit all URLs concurrently."""
semaphore = asyncio.Semaphore(self.concurrency)
all_issues = []
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
}
async with httpx.AsyncClient(verify=False, headers=headers, timeout=self.timeout) as client:
tasks = [self.audit_url(client, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, list):
all_issues.extend(result)
return all_issues
def run(self, urls: List[str] = None, input_file: str = None, output_file: str = 'headers_audit.csv') -> List[Dict]:
"""Run headers audit."""
import re # needed for HSTS check
# patch re into analyze_security_headers scope
globals()['re'] = re
if input_file and not urls:
try:
with open(input_file) as f:
urls = [line.strip() for line in f if line.strip()]
except Exception as e:
print(colored(f"[!] Error reading input file: {e}", "red"))
return []
if not urls:
print(colored("[!] No URLs provided for headers audit", "red"))
return []
if not self.silent:
print(colored(f"\n[+] Starting Headers/CORS Audit on {len(urls)} URLs...", "blue"))
findings = asyncio.run(self.audit_all(urls))
self.findings = findings
high = len([f for f in findings if f.get('severity') == 'High'])
med = len([f for f in findings if f.get('severity') == 'Medium'])
if not self.silent:
print(colored(f"\n[✓] Headers audit complete. {len(findings)} issues found ({high} High, {med} Medium)", "green"))
if findings:
os.makedirs(os.path.dirname(output_file) if os.path.dirname(output_file) else '.', exist_ok=True)
with open(output_file, 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['url', 'category', 'severity', 'issue', 'header', 'detail'])
writer.writeheader()
writer.writerows(findings)
if not self.silent:
print(colored(f"[✓] Results saved to {output_file}", "green"))
return findings
if __name__ == '__main__':
import sys
import re
urls = sys.argv[1:] or ['https://example.com']
auditor = HeadersAuditor()
auditor.run(urls=urls)