1from __future__ import annotations
2
3from typing import TYPE_CHECKING
4
5from ..results import AuditResult, CheckResult
6from .base import Audit
7
8if TYPE_CHECKING:
9 from ..scanner import Scanner
10
11
12class ReferrerPolicyAudit(Audit):
13 """Referrer-Policy header checks."""
14
15 name = "Referrer-Policy"
16 slug = "referrer-policy"
17 required = False # Privacy-focused rather than critical security
18 description = "Controls how much referrer information is sent with requests to protect user privacy. See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy"
19
20 def check(self, scanner: Scanner) -> AuditResult:
21 """Check if Referrer-Policy header is present and configured securely."""
22 response = scanner.fetch()
23
24 # Check if header is present
25 header = response.headers.get("Referrer-Policy")
26
27 if not header:
28 # Header not detected
29 return AuditResult(
30 name=self.name,
31 detected=False,
32 required=self.required,
33 checks=[],
34 description=self.description,
35 )
36
37 # Header detected - validate value
38 checks = [
39 self._check_policy_value(header),
40 ]
41
42 return AuditResult(
43 name=self.name,
44 detected=True,
45 required=self.required,
46 checks=checks,
47 description=self.description,
48 )
49
50 def _check_policy_value(self, header: str) -> CheckResult:
51 """Check if Referrer-Policy uses a secure value.
52
53 For fallback chains (comma-separated values), the rightmost value
54 is the preferred policy used by modern browsers, with earlier values
55 as fallbacks for older browsers.
56 """
57 # Can have multiple comma-separated values (fallback chain)
58 policies = [p.strip().lower() for p in header.split(",")]
59
60 # Secure/recommended policies
61 secure_policies = {
62 "no-referrer",
63 "strict-origin",
64 "strict-origin-when-cross-origin",
65 "same-origin",
66 }
67
68 # Acceptable but less ideal (OK for most sites)
69 # Note: no-referrer-when-downgrade is the browser default and used by many
70 # security-conscious sites. It's not a security issue, just less privacy-preserving.
71 acceptable_policies = {
72 "origin",
73 "origin-when-cross-origin",
74 "no-referrer-when-downgrade",
75 }
76
77 # Permissive/problematic policies
78 permissive_policies = {
79 "unsafe-url",
80 }
81
82 # Check the last (preferred) policy in the chain
83 primary_policy = policies[-1]
84
85 if primary_policy in secure_policies:
86 return CheckResult(
87 name="policy",
88 passed=True,
89 message=f"Referrer-Policy uses secure value: {primary_policy}",
90 )
91
92 if primary_policy in acceptable_policies:
93 # Acceptable but not ideal
94 return CheckResult(
95 name="policy",
96 passed=True,
97 message=f"Referrer-Policy uses acceptable value: {primary_policy}",
98 )
99
100 if primary_policy in permissive_policies:
101 return CheckResult(
102 name="policy",
103 passed=False,
104 message=f"Referrer-Policy uses permissive value that leaks information: {primary_policy}",
105 )
106
107 # Unknown or invalid policy
108 return CheckResult(
109 name="policy",
110 passed=False,
111 message=f"Referrer-Policy has unrecognized value: {primary_policy}",
112 )