summaryrefslogtreecommitdiff
path: root/django/utils/csp.py
blob: 08aaed685af77c47691dfe3c46f4256a34ec0fb8 (plain)
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
import secrets
from enum import StrEnum

from django.utils.functional import SimpleLazyObject, empty
from django.utils.html import format_html

# Template context key for the CSP nonce.
CONTEXT_KEY = "csp_nonce"


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 {% csp_nonce_attr %}></script>

    ``{% csp_nonce_attr %}`` will only render the nonce attribute if the nonce
    has been evaluated (i.e. accessed) elsewhere in the request/response cycle.

    """

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

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


def nonce_attr(context, media=None):
    nonce = context.get(CONTEXT_KEY)
    if media:
        return media.render(attrs={"nonce": nonce} if nonce is not None else None)
    if nonce is None:
        return ""
    return format_html('nonce="{}"', nonce)


def generate_nonce():
    return secrets.token_urlsafe(16)


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)