SentinelScan-WSS / backend /scanners /cookie_scanner.py
larxius's picture
Deploy SentinelScan WSS to HF Spaces
d543fc1 verified
Raw
History Blame Contribute Delete
17.6 kB
"""
cookie_scanner.py — Cookie Security Flags Auditor
==================================================
Performs a deep audit of all Set-Cookie headers across the site:
- Secure flag (absent on HTTPS)
- HttpOnly flag (missing XSS protection)
- SameSite attribute (None / Lax / Strict / absent)
- __Host- / __Secure- prefix compliance
- Excessive cookie lifetime (> 1 year)
- Sensitive names without protection
- Cookie scoping (Domain= too broad)
- Path= attribute
"""
import re, ssl, urllib.request, urllib.error, urllib.parse
from scanners.base_scanner import BaseScanner
# Cookie names that strongly suggest session / auth usage
SENSITIVE_NAMES = re.compile(
r"(sess|session|auth|token|jwt|access|refresh|user|uid|account|login|"
r"remember|csrf|xsrf|cart|order|payment)", re.I
)
MAX_SAFE_AGE_SECONDS = 365 * 24 * 3600 # 1 year
class CookieScanner(BaseScanner):
SCANNER_NAME = "Cookie Security Auditor"
_SCANNER_KEY = "cookie"
def __init__(self, scan_id, target, domain, **kwargs):
super().__init__(scan_id, target, domain, **kwargs)
self._is_https = self.target.startswith("https://")
# ------------------------------------------------------------------
def run(self) -> list:
self.log("INFO", f"[Cookies] Starting cookie security audit on {self.target}...")
try:
endpoints = self._crawl()
seen_cookies: set = set()
for url in endpoints:
cookies = self._collect_cookies(url)
for name, attrs, raw in cookies:
key = (name.lower(), url)
if key in seen_cookies:
continue
seen_cookies.add(key)
self._audit_cookie(name, attrs, raw, url)
self._check_cookie_scope(url if endpoints else self.target)
self._check_session_without_expiration()
self._check_persistent_cookies()
except Exception as e:
self.log("WARNING", f"[Cookies] Audit error: {e}")
self.log(
"SUCCESS" if not self.vulns else "WARNING",
f"[Cookies] Audit complete. {len(self.vulns)} issue(s) found.",
)
return self.vulns
# ------------------------------------------------------------------
def _crawl(self) -> list:
try:
# GAP-ADV: Centralized context
if self.discovery_context and "urls" in self.discovery_context:
return [u.get("url") if isinstance(u, dict) else u for u in self.discovery_context["urls"]]
return [self.target][:20]
except Exception as e:
self.log("ERROR", f"[Cookies] _crawl error: {e}")
return [self.target]
# ------------------------------------------------------------------
def _collect_cookies(self, url: str) -> list:
"""Return list of (name, attrs_dict, raw_header) tuples."""
cookies = []
try:
headers = {"User-Agent": "LarShield/2.0 Cookie-Auditor"}
headers.update(self.auth_headers or {})
body, status, resp_headers = self._make_request(url, headers=headers, timeout=8, return_response_obj=True)
raw_headers = resp_headers.get_all("Set-Cookie") if hasattr(resp_headers, "get_all") else []
if raw_headers is None: raw_headers = []
for raw in raw_headers:
name, attrs = self._parse_cookie(raw)
if name:
cookies.append((name, attrs, raw))
except Exception as e:
self.log("ERROR", f"[Cookies] _collect_cookies error: {e}")
return cookies
# ------------------------------------------------------------------
@staticmethod
def _parse_cookie(raw: str) -> tuple:
"""Parse a Set-Cookie header into (name, attrs_dict)."""
parts = [p.strip() for p in raw.split(";")]
if not parts:
return None, {}
name_val = parts[0].split("=", 1)
name = name_val[0].strip()
attrs = {}
for part in parts[1:]:
kv = part.split("=", 1)
attrs[kv[0].strip().lower()] = kv[1].strip() if len(kv) > 1 else True
return name, attrs
# ------------------------------------------------------------------
def _audit_cookie(self, name: str, attrs: dict, raw: str, url: str):
is_sensitive = bool(SENSITIVE_NAMES.search(name))
issues = []
# ── Secure flag ───────────────────────────────────────────────
if self._is_https and "secure" not in attrs:
issues.append("missing Secure flag")
self.add_vuln(
title=f"Cookie '{name}' Missing Secure Flag",
severity="Medium",
category="Cookie Security",
cvss_score=5.3,
description=(
f"The cookie `{name}` set at `{url}` does not have the `Secure` flag. "
"On an HTTPS site, omitting Secure allows the cookie to be transmitted "
"over plain HTTP, exposing it to network eavesdroppers."
),
remediation=(
f"Set-Cookie: {name}=<value>; Secure; HttpOnly; SameSite=Strict\n"
"Ensure all cookies on HTTPS sites include the Secure attribute."
),
)
# ── HttpOnly flag ─────────────────────────────────────────────
if "httponly" not in attrs and is_sensitive:
issues.append("missing HttpOnly flag on sensitive cookie")
self.add_vuln(
title=f"Sensitive Cookie '{name}' Missing HttpOnly Flag",
severity="High",
category="Cookie Security",
cvss_score=7.4,
description=(
f"The cookie `{name}` at `{url}` appears to be a session/auth cookie "
"(its name matches sensitive patterns) but does not have the `HttpOnly` "
"flag set. This allows JavaScript code to read it via `document.cookie`, "
"making it trivially stealable via any XSS vulnerability."
),
remediation=(
f"Set-Cookie: {name}=<value>; HttpOnly; Secure; SameSite=Strict\n"
"Add HttpOnly to all session and authentication cookies."
),
)
# ── SameSite attribute ────────────────────────────────────────
samesite = attrs.get("samesite", None)
if samesite is None:
issues.append("missing SameSite attribute")
sev = "High" if is_sensitive else "Medium"
self.add_vuln(
title=f"Cookie '{name}' Missing SameSite Attribute",
severity=sev,
category="Cookie Security",
cvss_score=6.5 if is_sensitive else 4.3,
description=(
f"The cookie `{name}` at `{url}` has no `SameSite` attribute. "
"Without SameSite, the cookie is sent on all cross-site requests, "
"enabling Cross-Site Request Forgery (CSRF) attacks."
),
remediation=(
f"Set-Cookie: {name}=<value>; SameSite=Strict; Secure; HttpOnly\n"
"Use SameSite=Strict for session cookies, Lax for others."
),
)
elif samesite.lower() == "none":
if "secure" not in attrs:
self.add_vuln(
title=f"Cookie '{name}' SameSite=None Without Secure",
severity="High",
category="Cookie Security",
cvss_score=7.5,
description=(
f"Cookie `{name}` at `{url}` has `SameSite=None` but is missing "
"the `Secure` flag. Modern browsers reject such cookies; when they "
"fall back to legacy behaviour, they become vulnerable to CSRF."
),
remediation="Set-Cookie: SameSite=None; Secure",
)
# ── Cookie lifetime ───────────────────────────────────────────
max_age = attrs.get("max-age")
if max_age and max_age is not True:
try:
if int(max_age) > MAX_SAFE_AGE_SECONDS:
self.add_vuln(
title=f"Cookie '{name}' Has Excessive Lifetime",
severity="Low",
category="Cookie Security",
cvss_score=3.1,
description=(
f"Cookie `{name}` has Max-Age={max_age}s "
f"(>{MAX_SAFE_AGE_SECONDS // (365*3600*24)} years). "
"Long-lived session cookies increase the window of attack "
"after a user's session is compromised or stolen."
),
remediation=(
"Limit session cookie lifetime to the session duration.\n"
"Use short Max-Age values and implement server-side session expiry."
),
)
except ValueError:
pass
# ── __Host- / __Secure- prefix compliance ─────────────────────
if name.startswith("__Host-"):
if "secure" not in attrs or "domain" in attrs or attrs.get("path","") != "/":
self.add_vuln(
title=f"__Host- Cookie '{name}' Prefix Violation",
severity="Medium",
category="Cookie Security",
cvss_score=5.3,
description=(
f"Cookie `{name}` uses the `__Host-` prefix but violates its "
"requirements (must have Secure, no Domain=, Path=/)."
),
remediation=(
"For __Host- cookies: set Secure, omit Domain=, set Path=/."
),
)
if name.startswith("__Secure-") and "secure" not in attrs:
self.add_vuln(
title=f"__Secure- Cookie '{name}' Missing Secure Flag",
severity="Medium",
category="Cookie Security",
cvss_score=5.3,
description=(
f"Cookie `{name}` uses the `__Secure-` prefix but is missing the "
"`Secure` flag, violating the prefix contract."
),
remediation="Add the Secure flag to all __Secure- prefixed cookies.",
)
if not issues:
self.log("SUCCESS", f"[Cookies] Cookie '{name}' — all flags OK")
else:
self.log("WARNING", f"[Cookies] Cookie '{name}' issues: {', '.join(issues)}")
# ------------------------------------------------------------------
def _check_cookie_scope(self, url: str):
"""Analyze cookie Domain and Path attributes for overly broad scoping."""
cookies = self._collect_cookies(url)
target_domain = urllib.parse.urlparse(self.target).hostname or ""
for name, attrs, raw in cookies:
# Check Domain attribute
domain = attrs.get("domain", "")
if domain:
domain = domain.lstrip(".")
if target_domain and domain != target_domain and not target_domain.endswith("." + domain):
self.add_vuln(
title=f"Cookie '{name}' Has Overly Broad Domain Scope",
severity="Medium",
category="Cookie Security",
cvss_score=5.3,
description=f"The cookie `{name}` has Domain=`{domain}` which is broader "
f"than the target domain `{target_domain}`. Cookies scoped to a broader "
"domain are sent to all subdomains, increasing exposure.",
evidence=f"Domain={domain}, Target={target_domain}",
request_details=f"Set-Cookie: {raw[:100]}",
confidence="High",
remediation="1. Set Domain to the exact origin domain.\n"
"2. Avoid broad domain scoping for session cookies.\n"
"3. Use __Host- prefix cookies which forbid Domain attribute.",
)
# Check Path attribute
path = attrs.get("path", "/")
if path == "/":
self.add_vuln(
title=f"Cookie '{name}' Has Root Path Scope (Path=/)",
severity="Low",
category="Cookie Security",
cvss_score=2.6,
description=f"The cookie `{name}` has Path=/ which means it is sent to "
"all endpoints on the domain. Consider restricting the path to reduce "
"the cookie's exposure.",
evidence=f"Path={path}",
request_details=f"Set-Cookie: {raw[:100]}",
confidence="Medium",
remediation="1. Set Path to the specific application path.\n"
"2. For admin-only cookies, use Path=/admin.\n"
"3. Use __Host- prefix cookies which require Path=/ only.",
)
# ------------------------------------------------------------------
def _check_session_without_expiration(self):
"""Check if session cookies have no expiration (should be session-scoped)."""
session_keywords = re.compile(r"(sess|session|sid|token|auth|jwt)", re.I)
cookies = self._collect_cookies(self.target)
for name, attrs, raw in cookies:
if not session_keywords.search(name):
continue
has_max_age = "max-age" in attrs
has_expires = "expires" in attrs
if not has_max_age and not has_expires:
self.add_vuln(
title=f"Session Cookie '{name}' Has No Expiration",
severity="Medium",
category="Cookie Security",
cvss_score=5.3,
description=f"The session cookie `{name}` has no Max-Age or Expires attribute. "
"While this likely means it is a session cookie (deleted on browser close), "
"it should be explicitly configured for clarity and consistency.",
evidence=f"Set-Cookie: {raw[:100]}",
request_details=f"GET {self.target}",
response_details=f"Set-Cookie: {raw[:100]}",
confidence="Info",
remediation="1. Explicitly set session cookie: remove Max-Age/Expires.\n"
"2. For persistent sessions, set reasonable Max-Age.\n"
"3. Implement server-side session expiry as a fallback.",
)
# ------------------------------------------------------------------
def _check_persistent_cookies(self):
"""Detect cookies with long expiration (persistent cookies) that may be risky."""
cookies = self._collect_cookies(self.target)
for name, attrs, raw in cookies:
max_age = attrs.get("max-age")
expires = attrs.get("expires")
duration_days = 0
if max_age and max_age is not True:
try:
duration_days = int(max_age) / 86400
except (ValueError, TypeError):
pass
if duration_days > 30:
self.add_vuln(
title=f"Persistent Cookie '{name}' With Long Expiration ({duration_days:.0f} days)",
severity="Low",
category="Cookie Security",
cvss_score=3.1,
description=f"The cookie `{name}` has a Max-Age of {int(duration_days)} days. "
"Persistent cookies with long lifetimes increase the risk of session hijacking "
"and should be audited for necessity.",
evidence=f"Max-Age={max_age}s ({duration_days:.0f} days)",
request_details=f"Set-Cookie: {raw[:100]}",
confidence="Medium",
remediation="1. Use session cookies (no expiration) for authentication.\n"
"2. Limit persistent cookie lifetime to a maximum of 30 days.\n"
"3. Implement refresh token rotation for long-lived sessions.",
)