SentinelScan-WSS / backend /scanners /cache_control_scanner.py
larxius's picture
Deploy SentinelScan WSS to HF Spaces
d543fc1 verified
Raw
History Blame Contribute Delete
3.3 kB
"""
cache_control_scanner.py — Browser Cache Control Scanner
"""
import urllib.request, urllib.error
from scanners.base_scanner import BaseScanner
SENSITIVE_PATHS = [
"/account", "/profile", "/dashboard", "/settings", "/api/me",
"/api/user", "/admin", "/invoices", "/billing", "/orders",
"/payment", "/statements", "/reports",
]
class CacheControlScanner(BaseScanner):
SCANNER_NAME = "Browser Cache Control Scanner"
_SCANNER_KEY = "cache_control"
def __init__(self, scan_id, target, domain, **kwargs):
super().__init__(scan_id, target, domain, **kwargs)
def run(self) -> list:
self.log("INFO", f"[CacheControl] Checking Cache-Control headers on sensitive pages of {self.target}...")
base = self.target.rstrip("/")
risky = []
# Check main page
for path in [""] + SENSITIVE_PATHS:
url = base + path if path else base
headers, status = self._get_headers(url)
if status not in (200, 302): continue
cc = headers.get("cache-control", "").lower()
pragma = headers.get("pragma", "").lower()
ct = headers.get("content-type", "").lower()
# Skip non-HTML/JSON (images, CSS, etc.)
if any(t in ct for t in ("image/", "text/css", "font/", "javascript")): continue
# Vulnerable if no-store is absent
if "no-store" not in cc:
risky.append({
"url": url, "cache-control": cc or "(missing)",
"pragma": pragma or "(missing)", "status": status
})
if risky:
self.add_vuln(
title=f"Sensitive Pages Cacheable by Browser ({len(risky)} pages)",
severity="Medium",
category="Information Disclosure",
cvss_score=5.3,
description="The following authenticated/sensitive pages lack `Cache-Control: no-store`, "
"allowing browsers and shared proxies to cache the responses. On shared/public devices, "
"a subsequent user can press Back or access the browser cache to retrieve private data:\n\n" +
"\n".join(f"- `{r['url']}` — Cache-Control: `{r['cache-control']}`" for r in risky[:8]),
remediation="Add to all authenticated responses:\n"
"`Cache-Control: no-store, no-cache, must-revalidate, max-age=0`\n"
"`Pragma: no-cache`\n"
"`Expires: 0`",
)
else:
self.log("SUCCESS", "[CacheControl] All sensitive pages properly set Cache-Control: no-store.")
return self.vulns
def _get_headers(self, url):
try:
req = urllib.request.Request(url, headers=self._make_headers())
with urllib.request.urlopen(req, timeout=5, context=self.get_ssl_context()) as r:
return {k.lower(): v for k, v in r.headers.items()}, r.status
except urllib.error.HTTPError as e:
return {k.lower(): v for k, v in e.headers.items()}, e.code
except Exception as e:
self.log("ERROR", f"[CacheControl] _get_headers error: {e}")
return {}, 0