File: csp.py

package info (click to toggle)
python-django 3%3A6.0~alpha1-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental
  • size: 62,204 kB
  • sloc: python: 370,694; javascript: 19,376; xml: 211; makefile: 187; sh: 28
file content (114 lines) | stat: -rw-r--r-- 3,565 bytes parent folder | download
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
import secrets
from enum import StrEnum

from django.utils.functional import SimpleLazyObject, empty


class CSP(StrEnum):
    """
    Content Security Policy constants for directive values and special tokens.

    These constants represent:
    1. Standard quoted string values from the CSP spec (e.g., 'self',
       'unsafe-inline')
    2. Special placeholder tokens (NONCE) that get replaced by the middleware

    Using this enum instead of raw strings provides better type checking,
    autocompletion, and protection against common mistakes like:

    - Typos (e.g., 'noone' instead of 'none')
    - Missing quotes (e.g., ["self"] instead of ["'self'"])
    - Inconsistent quote styles (e.g., ["'self'", "\"unsafe-inline\""])

    Example usage in Django settings:

        SECURE_CSP = {
            "default-src": [CSP.NONE],
            "script-src": [CSP.SELF, CSP.NONCE],
        }

    """

    # HTTP Headers.
    HEADER_ENFORCE = "Content-Security-Policy"
    HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only"

    # Standard CSP directive values.
    NONE = "'none'"
    REPORT_SAMPLE = "'report-sample'"
    SELF = "'self'"
    STRICT_DYNAMIC = "'strict-dynamic'"
    UNSAFE_EVAL = "'unsafe-eval'"
    UNSAFE_HASHES = "'unsafe-hashes'"
    UNSAFE_INLINE = "'unsafe-inline'"
    WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'"

    # Special placeholder that gets replaced by the middleware.
    # The value itself is arbitrary and should not be mistaken for a real
    # nonce.
    NONCE = "<CSP_NONCE_SENTINEL>"


class LazyNonce(SimpleLazyObject):
    """
    Lazily generates a cryptographically secure nonce string, for use in CSP
    headers.

    The nonce is only generated when first accessed (e.g., via string
    interpolation or inside a template).

    The nonce will evaluate as `True` if it has been generated, and `False` if
    it has not. This is useful for third-party Django libraries that want to
    support CSP without requiring it.

    Example Django template usage with context processors enabled:

        <script{% if csp_nonce %} nonce="{{ csp_nonce }}"...{% endif %}>

    The `{% if %}` block will only render if the nonce has been evaluated
    elsewhere.

    """

    def __init__(self):
        super().__init__(self._generate)

    def _generate(self):
        return secrets.token_urlsafe(16)

    def __bool__(self):
        return self._wrapped is not empty


def build_policy(config, nonce=None):
    policy = []

    for directive, values in config.items():
        if values in (None, False):
            continue

        if values is True:
            rendered_value = ""
        else:
            if isinstance(values, set):
                # Sort values for consistency, preventing cache invalidation
                # between requests and ensuring reliable browser caching.
                values = sorted(values)
            elif not isinstance(values, list | tuple):
                values = [values]

            # Replace the nonce sentinel with the actual nonce values, if the
            # sentinel is found and a nonce is provided. Otherwise, remove it.
            if (has_sentinel := CSP.NONCE in values) and nonce:
                values = [f"'nonce-{nonce}'" if v == CSP.NONCE else v for v in values]
            elif has_sentinel:
                values = [v for v in values if v != CSP.NONCE]

            if not values:
                continue

            rendered_value = " ".join(values)

        policy.append(f"{directive} {rendered_value}".rstrip())

    return "; ".join(policy)