Plain is headed towards 1.0! Subscribe for development updates →

  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        )