File size: 17,598 Bytes
d543fc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
"""

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.",
                )