pax_global_header00006660000000000000000000000064147732543470014532gustar00rootroot0000000000000052 comment=6d5cc6c00e90651447ee555f948231bc21998b3e mozilla-django-csp-6d5cc6c/000077500000000000000000000000001477325434700157235ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/.coveragerc000066400000000000000000000001061477325434700200410ustar00rootroot00000000000000[run] source = csp omit = csp/tests/* [report] show_missing = True mozilla-django-csp-6d5cc6c/.github/000077500000000000000000000000001477325434700172635ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/.github/workflows/000077500000000000000000000000001477325434700213205ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/.github/workflows/ci.yaml000066400000000000000000000026671477325434700226120ustar00rootroot00000000000000name: "CI" # Note that this name appears in the README's badge on: push: branches: - main workflow_dispatch: pull_request: release: types: [published] jobs: run-tests: runs-on: ubuntu-latest strategy: matrix: python-version: - '3.9' - '3.10' - '3.11' - '3.12' - '3.13' - 'pypy-3.9' - 'pypy-3.10' steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install tox tox-gh-actions - name: Test with tox run: tox release: name: Release django-csp if: github.event_name == 'release' && github.event.action == 'published' needs: - run-tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: 3.12 - name: Install dependencies for package building only run: pip install build - name: Build package for upload to PyPI run: python -m build . - name: Upload the distribution to PyPI uses: pypa/gh-action-pypi-publish@v1.4.2 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} mozilla-django-csp-6d5cc6c/.gitignore000066400000000000000000000001371477325434700177140ustar00rootroot00000000000000*.py[co] *.egg-info *.db *.sw[po] .cache .coverage .python-version .tox dist build docs/_build mozilla-django-csp-6d5cc6c/.pre-commit-config.yaml000066400000000000000000000017731477325434700222140ustar00rootroot00000000000000# Global excludes, override per repo below if different excludes required. # exclude: > # (?x)^( # DIRNAME_OR_FILENAME_HERE # | DIRNAME_OR_FILENAME_HERE # | DIRNAME_OR_FILENAME_HERE # ) repos: # Note: hooks that add content must run before ones which check formatting, lint, etc - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.2 hooks: # Run the linter - id: ruff args: [--fix, --exit-non-zero-on-fix] # Run the formatter - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt rev: v2.5.1 hooks: - id: pyproject-fmt - repo: https://github.com/adamchainz/blacken-docs rev: 1.19.1 hooks: - id: blacken-docs additional_dependencies: - black==24.4.2 files: 'docs/.*\.rst$' args: ["--rst-literal-block"] mozilla-django-csp-6d5cc6c/.readthedocs.yaml000066400000000000000000000010211477325434700211440ustar00rootroot00000000000000# Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-24.04 tools: python: "3.13" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py formats: - pdf # Optionally declare the Python requirements required to build your docs python: install: - method: pip path: . - requirements: docs/requirements.txt mozilla-django-csp-6d5cc6c/CHANGES.md000066400000000000000000000303431477325434700173200ustar00rootroot00000000000000CHANGES ======= 4.0 === This release contains several breaking changes. For a complete migration guide, see: https://django-csp.readthedocs.io/en/latest/migration-guide.html ## Breaking Changes - **Configuration Format**: Moved to dict-based configuration which allows for setting policies for both enforced and report-only. Instead of using individual settings with `CSP_` prefixes, you now use dictionaries called `CONTENT_SECURITY_POLICY` and/or `CONTENT_SECURITY_POLICY_REPORT_ONLY`. ([#219](https://github.com/mozilla/django-csp/pull/219)) You can use Django's check command to automatically identify existing CSP settings and generate a template for the new configuration format: ``` python manage.py check ``` This will detect your old `CSP_` prefixed settings and output a draft of the new dict-based configuration, giving you a starting point for migration. **Example:** Change from: ```python CSP_DEFAULT_SRC = ["'self'", "*.example.com"] CSP_SCRIPT_SRC = ["'self'", "js.cdn.com/example/"] CSP_IMG_SRC = ["'self'", "data:", "example.com"] CSP_EXCLUDE_URL_PREFIXES = ["/admin"] ``` to: ```python from csp.constants import SELF CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "default-src": [SELF, "*.example.com"], "script-src": [SELF, "js.cdn.com/example/"], "img-src": [SELF, "data:", "example.com"], }, "EXCLUDE_URL_PREFIXES": ["/admin"], } ``` - **Nonce Configuration**: Switched from specifying directives that should contain nonces as a separate list to using a sentinel `NONCE` value in the directive itself. ([#223](https://github.com/mozilla/django-csp/pull/223)) **Example:** Change from: ```python CSP_INCLUDE_NONCE_IN = ['script-src', 'style-src'] ``` to: ```python from csp.constants import NONCE, SELF CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "script-src": [SELF, NONCE], "style-src": [SELF, NONCE], } } ``` - **Nonce Behavior**: Changed how `request.csp_nonce` works - it is now Falsy (`bool(request.csp_nonce)`) until it is read as a string (e.g., used in a template or with `str(request.csp_nonce)`). Previously, it always tested as `True`, and testing generated the nonce. ([#270](https://github.com/mozilla/django-csp/pull/270)) **Before:** ```python # The nonce was generated when this was evaluated if request.csp_nonce: # Do something with nonce ``` **After:** ```python # This won't generate the nonce, and will evaluate to False until nonce is read as a string if request.csp_nonce: # This code won't run until nonce is used as a string # To generate and use the nonce nonce_value = str(request.csp_nonce) ``` - Dropped support for Django ≤3.2. - Dropped support for Python 3.8. ## New Features and Improvements - **Dual Policy Support**: Added support for enforced and report-only policies simultaneously using the separate `CONTENT_SECURITY_POLICY` and `CONTENT_SECURITY_POLICY_REPORT_ONLY` settings. **Example:** ```python from csp.constants import NONE, SELF # Enforced policy CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "default-src": [SELF, "cdn.example.net"], "frame-ancestors": [SELF], }, } # Report-only policy (stricter for testing) CONTENT_SECURITY_POLICY_REPORT_ONLY = { "DIRECTIVES": { "default-src": [NONE], "script-src": [SELF], "style-src": [SELF], "report-uri": "https://example.com/csp-report/", }, } ``` - **CSP Constants**: Added CSP keyword constants in `csp.constants` (e.g., `SELF` instead of `"'self'"`) to minimize quoting mistakes and typos. ([#222](https://github.com/mozilla/django-csp/pull/222)) **Example:** Change from: ```python CSP_DEFAULT_SRC = ["'self'", "'none'"] ``` to: ```python from csp.constants import SELF, NONE CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "default-src": [SELF, NONE], # No need to worry about quoting } } ``` - Added comprehensive type hints. ([#228](https://github.com/mozilla/django-csp/pull/228)) - Added `EXCLUDE_URL_PREFIXES` check not a string. ([#252](https://github.com/mozilla/django-csp/pull/252)) - Added support for CSP configuration as sets. ([#251](https://github.com/mozilla/django-csp/pull/251)) - Changed `REPORT_PERCENTAGE` to be a float between `0.0` and `100.0` and improved behavior for 100% report percentage to always send CSP reports. - Added ability to read the nonce after response if it was included in the header. This will raise an error when nonce is accessed after response if not already generated. ([#269](https://github.com/mozilla/django-csp/pull/269)) - Made changes to simplify middleware logic and make `CSPMiddleware` easier to subclass. The updated middleware returns a PolicyParts dataclass that can be modified before the policy is built. ([#237](https://github.com/mozilla/django-csp/pull/237)) ## Other Changes - Added Python 3.13 support. - Added support for Django 5.1 and 5.2. - Documentation improvements including fixed trusted_types links and clarification on NONE vs Python's None. - Documentation note that reporting percentage needs rate limiting middleware. - Expanded ruff configuration and moved into pyproject.toml. 4.0b7 ===== - Removed ``CSPMiddlewareAlwaysGenerateNonce`` middleware that forced nonce headers when not used in content encouraging better security practices ([#274](https://github.com/mozilla/django-csp/pull/274)) 4.0b6 ===== - Fix ``CSPMiddlewareAlwaysGenerateNonce`` to always generate the nonce. ([#272](https://github.com/mozilla/django-csp/pull/272)) 4.0b5 ===== BACKWARDS INCOMPATIBLE change: - `request.csp_nonce` is now Falsy (`bool(request.csp_nonce)`) until it is read as a string (for example, used in a template, or `str(request.csp_nonce)`). Previously, it always tested as `True`, and testing generated the nonce. ([#270](https://github.com/mozilla/django-csp/pull/270)) Other changes: - Upgrade ReadTheDocs environment ([#262](https://github.com/mozilla/django-csp/pull/262)) - Allow reading the nonce after response if it was included in the header. Add ``CSPMiddlewareAlwaysGenerateNonce`` to always generate a nonce. ([#269](https://github.com/mozilla/django-csp/pull/262)) 4.0b4 ===== - Fix missing packaging dependency ([#266](https://github.com/mozilla/django-csp/pull/266)) 4.0b3 ===== - Add Python 3.13, drop EOL Python 3.8 ([#245](https://github.com/mozilla/django-csp/pull/245)) - docs: Fix trusted_types links ([#250](https://github.com/mozilla/django-csp/pull/250)) - Add `EXCLUDE_URL_PREFIXES` check ([#252](https://github.com/mozilla/django-csp/pull/252)) - Support CSP configuration as sets ([#251](https://github.com/mozilla/django-csp/pull/251)) - docs: Note that reporting percentage needs rate limiting middleware ([#256](https://github.com/mozilla/django-csp/pull/256)) * Document constant NONE vs Python's None ([#255](https://github.com/mozilla/django-csp/pull/255)) - Raise error when nonce accessed after response ([#258](https://github.com/mozilla/django-csp/pull/258)) - Test on Django 5.2 ([#261](https://github.com/mozilla/django-csp/pull/261)) 4.0b2 ===== - Add type hints. ([#228](https://github.com/mozilla/django-csp/pull/228)) - Expand ruff configuration and move into pyproject.toml [[#234](https://github.com/mozilla/django-csp/pull/234)] - Documentation fixes by jamesbeith and jcari-dev - Simplify middleware logic ([#226](https://github.com/mozilla/django-csp/pull/226)) - Report percentage of 100% should always send CSP report ([#236](https://github.com/mozilla/django-csp/pull/236)) - Changes to make `CSPMiddleware` easier to subclass ([#237](https://github.com/mozilla/django-csp/pull/237)) - Change `REPORT_PERCENTAGE` to allow floats (e.g. for values < 1%) ([#242](https://github.com/mozilla/django-csp/pull/242)) - Add Django 5.1 support ([#243](https://github.com/mozilla/django-csp/pull/243)) 4.0b1 ===== BACKWARDS INCOMPATIBLE changes: - Move to dict-based configuration which allows for setting policies for both enforced and report-only. See the migration guide in the docs for migrating your settings. - Switch from specifying which directives should contain the nonce as a separate list, and instead use a sentinel `NONCE` in the directive itself. Other changes: - Add pyproject-fmt to pre-commit, and update pre-commit versions - Fixes #36: Add support for enforced and report-only policies simultaneously - Drop support for Django <=3.2, end of extended support - Add CSP keyword constants in `csp.constants`, e.g. to replace `"'self'"` with `SELF` 3.8 === Please note: this release folds in a number of fixups, upgrades and documentation tweaks, but is functionally the same as 3.7. New features will come with 3.9+ - Update Python syntax for modern versions with pyupgrade - Drop support for EOL Python <3.8 and Django <2.2 version; add support up to Django 5 on Python 3.12 - Switch to ruff instead of pep8 and flake8 - Move from CircleCI to Github Actions for CI - Add support for using pre-commit with the project - Remove deprecation warning for child-src - Fix capturing brackets in script template tags - Update docs to clarify when nonce will not be added to headers - Move from setup.py and setup.cfg to pyproject.toml (#209) Note: identical other than release packaging to 3.8rc1 3.8rc1 ====== - Move from setup.py and setup.cfg to pyproject.toml (#209) 3.8rc ===== Please note: this release folds in a number of fixups, upgrades and documentation tweaks, but is functionally the same as 3.7. New features will come with 3.9+ - Update Python syntax for modern versions with pyupgrade - Drop support for EOL Python <3.8 and Django <2.2 version; add support up to Django 5 on Python 3.12 - Switch to ruff instead of pep8 and flake8 - Move from CircleCI to Github Actions for CI - Add support for using pre-commit with the project - Remove deprecation warning for child-src - Fix capturing brackets in script template tags - Update docs to clarify when nonce will not be added to headers 3.7 === - Add support for Trusted Types - Use 128 bits base64 encoded for nonce 3.6 === - Add support/testing for Django 2.2 and 3.0 - Add support/testing for Python 3.7 and 3.8 - Disable CSP for Django NotFound debug view - Add new headers used in CSP level 3 - Add support for the report-to directive 3.5 === - New RateLimitedCSPMiddleware middleware (#97) - Add support for csp nonce and "script" template tag. (#78) - Various smaller fixes along the way 3.4 === - Remove support for Django 1.6 and 1.7 as they're out of life - Adds pypy3, Django 2.0.x and current Django master to our CI tests - Allow removing directives using @csp_replace - Add CSP nonce support 3.3 === - Add support for Django 1.11 - Add support for Python 3.6 3.2 === - Add manifest-src fetch directive - - Add worker-src fetch directive - - Add plugin-types document directive - - Add require-sri-for - - Add upgrade-insecure-requests - - Add block-all-mixed-content - - Add deprecation warning for child-src (#80) 3.1 === - Add support for Django 1.10 middlewares - Allow lazy objects to be assigned to CSP_REPORT_URI v3.0 ==== - Add support for Python 3 and PyPy - Move to pytest for testing - Add wheel build support - Drops support for Django < 1.6, adds support for Django 1.6, 1.7, 1.8 and 1.9 - Remove leftover references to the old report processing feature (#64) - Fix accidental mutation of config (#52) Please note that this is a big release that touches quite a few parts so please make sure you're testing thoroughly and report any issues to v2.0.3 ====== - Disable CSP on built-in error pages. v2.0.1 & v2.0.2 =============== No changes. I just can't package Python files. v2.0 ==== - Dropped report processing feature and code. - Complies with CSP v1.0 and v1.1 (excluding experimental features). - Dropped support for X-Content-Security-Policy and X-WebKit-CSP headers. mozilla-django-csp-6d5cc6c/CODE_OF_CONDUCT.md000066400000000000000000000012611477325434700205220ustar00rootroot00000000000000# Community Participation Guidelines This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details, please read the [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). ## How to Report For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. mozilla-django-csp-6d5cc6c/LICENSE000066400000000000000000000027651477325434700167420ustar00rootroot00000000000000Copyright (c) 2013, Mozilla Foundation All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of django-csp nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. mozilla-django-csp-6d5cc6c/MANIFEST.in000066400000000000000000000000431477325434700174560ustar00rootroot00000000000000include LICENSE include README.rst mozilla-django-csp-6d5cc6c/README.rst000066400000000000000000000012571477325434700174170ustar00rootroot00000000000000.. image:: https://badge.fury.io/py/django-csp.svg :target: https://pypi.python.org/pypi/django_csp .. image:: https://github.com/mozilla/django-csp/actions/workflows/ci.yaml/badge.svg :target: https://github.com/mozilla/django-csp/actions/workflows/ci.yaml ========== Django-CSP ========== Django-CSP adds Content-Security-Policy_ headers to Django. The code lives on GitHub_, where you can report Issues_. The full documentation is available on ReadTheDocs_. .. _Content-Security-Policy: http://www.w3.org/TR/CSP/ .. _GitHub: https://github.com/mozilla/django-csp .. _Issues: https://github.com/mozilla/django-csp/issues .. _ReadTheDocs: http://django-csp.readthedocs.org/ mozilla-django-csp-6d5cc6c/csp/000077500000000000000000000000001477325434700165105ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/__init__.py000066400000000000000000000000001477325434700206070ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/apps.py000066400000000000000000000002271477325434700200260ustar00rootroot00000000000000from django.apps import AppConfig from csp.checks import * # noqa: F403 (here to register the checks) class CspConfig(AppConfig): name = "csp" mozilla-django-csp-6d5cc6c/csp/checks.py000066400000000000000000000102421477325434700203210ustar00rootroot00000000000000from __future__ import annotations import pprint from collections.abc import Sequence from importlib.metadata import version from typing import TYPE_CHECKING, Any from django.conf import settings from django.core import checks from django.core.checks import Error, register from packaging.version import Version from csp.constants import NONCE if TYPE_CHECKING: from django.apps.config import AppConfig OUTDATED_SETTINGS = [ "CSP_CHILD_SRC", "CSP_CONNECT_SRC", "CSP_DEFAULT_SRC", "CSP_SCRIPT_SRC", "CSP_SCRIPT_SRC_ATTR", "CSP_SCRIPT_SRC_ELEM", "CSP_OBJECT_SRC", "CSP_STYLE_SRC", "CSP_STYLE_SRC_ATTR", "CSP_STYLE_SRC_ELEM", "CSP_FONT_SRC", "CSP_FRAME_SRC", "CSP_IMG_SRC", "CSP_MANIFEST_SRC", "CSP_MEDIA_SRC", "CSP_PREFETCH_SRC", "CSP_WORKER_SRC", "CSP_BASE_URI", "CSP_PLUGIN_TYPES", "CSP_SANDBOX", "CSP_FORM_ACTION", "CSP_FRAME_ANCESTORS", "CSP_NAVIGATE_TO", "CSP_REQUIRE_SRI_FOR", "CSP_REQUIRE_TRUSTED_TYPES_FOR", "CSP_TRUSTED_TYPES", "CSP_UPGRADE_INSECURE_REQUESTS", "CSP_BLOCK_ALL_MIXED_CONTENT", "CSP_REPORT_URI", "CSP_REPORT_TO", ] def migrate_settings() -> tuple[dict[str, Any], bool]: # This function is used to migrate settings from the old format to the new format. config: dict[str, Any] = { "DIRECTIVES": {}, } REPORT_ONLY = getattr(settings, "CSP_REPORT_ONLY", False) _EXCLUDE_URL_PREFIXES = getattr(settings, "CSP_EXCLUDE_URL_PREFIXES", None) if _EXCLUDE_URL_PREFIXES is not None: config["EXCLUDE_URL_PREFIXES"] = _EXCLUDE_URL_PREFIXES _REPORT_PERCENTAGE = getattr(settings, "CSP_REPORT_PERCENTAGE", None) if _REPORT_PERCENTAGE is not None: config["REPORT_PERCENTAGE"] = _REPORT_PERCENTAGE * 100 include_nonce_in = getattr(settings, "CSP_INCLUDE_NONCE_IN", []) for setting in OUTDATED_SETTINGS: if hasattr(settings, setting): directive = setting[4:].replace("_", "-").lower() value = getattr(settings, setting) if value: config["DIRECTIVES"][directive] = value if directive in include_nonce_in: config["DIRECTIVES"][directive].append(NONCE) return config, REPORT_ONLY @register(checks.Tags.security) def check_django_csp_lt_4_0(app_configs: Sequence[AppConfig] | None, **kwargs: Any) -> list[Error]: check_settings = OUTDATED_SETTINGS + ["CSP_REPORT_ONLY", "CSP_EXCLUDE_URL_PREFIXES", "CSP_REPORT_PERCENTAGE"] if any(hasattr(settings, setting) for setting in check_settings): # Try to build the new config. config, REPORT_ONLY = migrate_settings() warning = ( "You are using django-csp < 4.0 settings. Please update your settings to use the new format.\n" "See https://django-csp.readthedocs.io/en/latest/migration-guide.html for more information.\n\n" "We have attempted to build the new CSP config for you based on your current settings:\n\n" f"CONTENT_SECURITY_POLICY{'_REPORT_ONLY' if REPORT_ONLY else ''} = " + pprint.pformat(config, sort_dicts=True) ) return [Error(warning, id="csp.E001")] return [] @register(checks.Tags.security) def check_exclude_url_prefixes_is_not_string(app_configs: Sequence[AppConfig] | None, **kwargs: Any) -> list[Error]: """ Check that EXCLUDE_URL_PREFIXES in settings is not a string. If it is a string it can lead to a security issue where the string is treated as a list of characters, resulting in '/' matching all paths excluding the CSP header from all responses. """ # Skip check for django-csp < 4.0. if Version(version("django-csp")) < Version("4.0a1"): return [] errors = [] keys = ( "CONTENT_SECURITY_POLICY", "CONTENT_SECURITY_POLICY_REPORT_ONLY", ) for key in keys: config = getattr(settings, key, {}) if isinstance(config, dict) and isinstance(config.get("EXCLUDE_URL_PREFIXES"), str): errors.append( Error( f"EXCLUDE_URL_PREFIXES in {key} settings must be a list or tuple.", id="csp.E002", ) ) return errors mozilla-django-csp-6d5cc6c/csp/constants.py000066400000000000000000000013221477325434700210740ustar00rootroot00000000000000from typing import Any HEADER = "Content-Security-Policy" HEADER_REPORT_ONLY = "Content-Security-Policy-Report-Only" NONE = "'none'" REPORT_SAMPLE = "'report-sample'" SELF = "'self'" STRICT_DYNAMIC = "'strict-dynamic'" UNSAFE_ALLOW_REDIRECTS = "'unsafe-allow-redirects'" UNSAFE_EVAL = "'unsafe-eval'" UNSAFE_HASHES = "'unsafe-hashes'" UNSAFE_INLINE = "'unsafe-inline'" WASM_UNSAFE_EVAL = "'wasm-unsafe-eval'" class Nonce: _instance = None def __new__(cls: type["Nonce"], *args: Any, **kwargs: Any) -> "Nonce": if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance def __repr__(self) -> str: return "csp.constants.NONCE" NONCE = Nonce() mozilla-django-csp-6d5cc6c/csp/context_processors.py000066400000000000000000000004361477325434700230330ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Literal if TYPE_CHECKING: from django.http import HttpRequest def nonce(request: HttpRequest) -> dict[Literal["CSP_NONCE"], str]: nonce = getattr(request, "csp_nonce", "") return {"CSP_NONCE": nonce} mozilla-django-csp-6d5cc6c/csp/contrib/000077500000000000000000000000001477325434700201505ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/contrib/__init__.py000066400000000000000000000000001477325434700222470ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/contrib/rate_limiting.py000066400000000000000000000027361477325434700233610ustar00rootroot00000000000000from __future__ import annotations import random from typing import TYPE_CHECKING from django.conf import settings from csp.middleware import CSPMiddleware, PolicyParts if TYPE_CHECKING: from django.http import HttpRequest, HttpResponseBase class RateLimitedCSPMiddleware(CSPMiddleware): """A CSP middleware that rate-limits the number of violation reports sent to report-uri by excluding it from some requests.""" def get_policy_parts(self, request: HttpRequest, response: HttpResponseBase, report_only: bool = False) -> PolicyParts: policy_parts = super().get_policy_parts(request, response, report_only) csp_setting_name = "CONTENT_SECURITY_POLICY_REPORT_ONLY" if report_only else "CONTENT_SECURITY_POLICY" policy = getattr(settings, csp_setting_name, None) if policy is None: return policy_parts # `random.random` returns a value in the range [0.0, 1.0) so all values will be < 100.0. remove_report = random.random() * 100 >= policy.get("REPORT_PERCENTAGE", 100) if remove_report: if policy_parts.replace is None: policy_parts.replace = { "report-uri": None, "report-to": None, } else: policy_parts.replace.update( { "report-uri": None, "report-to": None, } ) return policy_parts mozilla-django-csp-6d5cc6c/csp/decorators.py000066400000000000000000000066061477325434700212370ustar00rootroot00000000000000from __future__ import annotations from functools import wraps from typing import TYPE_CHECKING, Any, Callable if TYPE_CHECKING: from django.http import HttpRequest, HttpResponseBase # A generic Django view function _VIEW_T = Callable[[HttpRequest], HttpResponseBase] _VIEW_DECORATOR_T = Callable[[_VIEW_T], _VIEW_T] def csp_exempt(REPORT_ONLY: bool | None = None) -> _VIEW_DECORATOR_T: if callable(REPORT_ONLY): raise RuntimeError( "Incompatible `csp_exempt` decorator usage. This decorator now requires arguments, " "even if none are passed. Change bare decorator usage (@csp_exempt) to parameterized " "decorator usage (@csp_exempt()). See the django-csp 4.0 migration guide for more " "information." ) def decorator(f: _VIEW_T) -> _VIEW_T: @wraps(f) def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: resp = f(*a, **kw) if REPORT_ONLY: setattr(resp, "_csp_exempt_ro", True) else: setattr(resp, "_csp_exempt", True) return resp return _wrapped return decorator # Error message for deprecated decorator arguments. DECORATOR_DEPRECATION_ERROR = ( "Incompatible `{fname}` decorator arguments. This decorator now takes a single dict argument. " "See the django-csp 4.0 migration guide for more information." ) def csp_update(config: dict[str, Any] | None = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: if config is None and kwargs: raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp_update")) def decorator(f: _VIEW_T) -> _VIEW_T: @wraps(f) def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: resp = f(*a, **kw) if REPORT_ONLY: setattr(resp, "_csp_update_ro", config) else: setattr(resp, "_csp_update", config) return resp return _wrapped return decorator def csp_replace(config: dict[str, Any] | None = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: if config is None and kwargs: raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp_replace")) def decorator(f: _VIEW_T) -> _VIEW_T: @wraps(f) def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: resp = f(*a, **kw) if REPORT_ONLY: setattr(resp, "_csp_replace_ro", config) else: setattr(resp, "_csp_replace", config) return resp return _wrapped return decorator def csp(config: dict[str, Any] | None = None, REPORT_ONLY: bool = False, **kwargs: Any) -> _VIEW_DECORATOR_T: if config is None and kwargs: raise RuntimeError(DECORATOR_DEPRECATION_ERROR.format(fname="csp")) if config is None: processed_config: dict[str, list[Any]] = {} else: processed_config = {k: [v] if isinstance(v, str) else v for k, v in config.items()} def decorator(f: _VIEW_T) -> _VIEW_T: @wraps(f) def _wrapped(*a: Any, **kw: Any) -> HttpResponseBase: resp = f(*a, **kw) if REPORT_ONLY: setattr(resp, "_csp_config_ro", processed_config) else: setattr(resp, "_csp_config", processed_config) return resp return _wrapped return decorator mozilla-django-csp-6d5cc6c/csp/exceptions.py000066400000000000000000000000511477325434700212370ustar00rootroot00000000000000class CSPNonceError(Exception): pass mozilla-django-csp-6d5cc6c/csp/extensions/000077500000000000000000000000001477325434700207075ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/extensions/__init__.py000066400000000000000000000035401477325434700230220ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING, Any, Callable from jinja2 import nodes from jinja2.ext import Extension from csp.utils import SCRIPT_ATTRS, build_script_tag if TYPE_CHECKING: from jinja2.parser import Parser class NoncedScript(Extension): # a set of names that trigger the extension. tags = {"script"} def parse(self, parser: Parser) -> nodes.Node: # the first token is the token that started the tag. In our case # we only listen to ``'script'`` so this will be a name token with # `script` as value. We get the line number so that we can give # that line number to the nodes we create by hand. lineno = next(parser.stream).lineno # Get the current context and pass along kwargs = [nodes.Keyword("ctx", nodes.ContextReference())] # Parse until we are done with optional script tag attributes while parser.stream.current.value in SCRIPT_ATTRS: attr_name = parser.stream.current.value parser.stream.skip(2) kwargs.append(nodes.Keyword(attr_name, parser.parse_expression())) # now we parse the body of the script block up to `endscript` and # drop the needle (which would always be `endscript` in that case) body = parser.parse_statements(("name:endscript",), drop_needle=True) # now return a `CallBlock` node that calls our _render_script # helper method on this extension. return nodes.CallBlock(self.call_method("_render_script", kwargs=kwargs), [], [], body).set_lineno(lineno) def _render_script(self, caller: Callable[[], str], **kwargs: Any) -> str: ctx = kwargs.pop("ctx") request = ctx.get("request") kwargs["nonce"] = str(request.csp_nonce) kwargs["content"] = caller().strip() return build_script_tag(**kwargs) mozilla-django-csp-6d5cc6c/csp/middleware.py000066400000000000000000000135601477325434700212040ustar00rootroot00000000000000from __future__ import annotations import base64 import http.client as http_client import os import warnings from dataclasses import asdict, dataclass from functools import partial from typing import TYPE_CHECKING from django.conf import settings from django.utils.deprecation import MiddlewareMixin from django.utils.functional import SimpleLazyObject, empty from csp.constants import HEADER, HEADER_REPORT_ONLY from csp.exceptions import CSPNonceError from csp.utils import DIRECTIVES_T, build_policy if TYPE_CHECKING: from django.http import HttpRequest, HttpResponseBase @dataclass class PolicyParts: # A dataclass is used rather than a namedtuple so that the attributes are mutable config: DIRECTIVES_T | None = None update: DIRECTIVES_T | None = None replace: DIRECTIVES_T | None = None nonce: str | None = None class CheckableLazyObject(SimpleLazyObject): """A SimpleLazyObject where bool(obj) returns True if no longer lazy""" def __bool__(self) -> bool: """ If the wrapped function has been evaluated, return True. If the wrapped function has not been evalated, return False. """ return getattr(self, "_wrapped") is not empty class CSPMiddleware(MiddlewareMixin): """ Implements the Content-Security-Policy response header, which conforming user-agents can use to restrict the permitted sources of various content. See http://www.w3.org/TR/CSP/ Can be customised by subclassing and extending the get_policy_parts method. """ def _make_nonce(self, request: HttpRequest) -> str: # Ensure that any subsequent calls to request.csp_nonce return the same value stored_nonce = getattr(request, "_csp_nonce", None) if isinstance(stored_nonce, str): return stored_nonce nonce = base64.b64encode(os.urandom(16)).decode("ascii") setattr(request, "_csp_nonce", nonce) return nonce @staticmethod def _csp_nonce_post_response() -> None: raise CSPNonceError( "The 'csp_nonce' attribute is not available after the CSP header has been written. Consider adjusting your MIDDLEWARE order." ) def process_request(self, request: HttpRequest) -> None: nonce = partial(self._make_nonce, request) setattr(request, "csp_nonce", CheckableLazyObject(nonce)) def process_response(self, request: HttpRequest, response: HttpResponseBase) -> HttpResponseBase: # Check for debug view exempted_debug_codes = ( http_client.INTERNAL_SERVER_ERROR, http_client.NOT_FOUND, ) if response.status_code in exempted_debug_codes and settings.DEBUG: return response policy_parts = self.get_policy_parts(request=request, response=response) csp = build_policy(**asdict(policy_parts)) if csp: # Only set header if not already set and not an excluded prefix and not exempted. is_not_exempt = getattr(response, "_csp_exempt", False) is False no_header = HEADER not in response policy = getattr(settings, "CONTENT_SECURITY_POLICY", None) or {} prefixes = policy.get("EXCLUDE_URL_PREFIXES", None) or () is_not_excluded = not request.path_info.startswith(tuple(prefixes)) if no_header and is_not_exempt and is_not_excluded: response[HEADER] = csp policy_parts_ro = self.get_policy_parts(request=request, response=response, report_only=True) csp_ro = build_policy(**asdict(policy_parts_ro), report_only=True) if csp_ro: # Only set header if not already set and not an excluded prefix and not exempted. is_not_exempt = getattr(response, "_csp_exempt_ro", False) is False no_header = HEADER_REPORT_ONLY not in response policy = getattr(settings, "CONTENT_SECURITY_POLICY_REPORT_ONLY", None) or {} prefixes = policy.get("EXCLUDE_URL_PREFIXES", None) or () is_not_excluded = not request.path_info.startswith(tuple(prefixes)) if no_header and is_not_exempt and is_not_excluded: response[HEADER_REPORT_ONLY] = csp_ro # Once we've written the header, accessing the `request.csp_nonce` will no longer trigger # the nonce to be added to the header. Instead we throw an error here to catch this since # this has security implications. if getattr(request, "_csp_nonce", None) is None: setattr(request, "csp_nonce", CheckableLazyObject(self._csp_nonce_post_response)) return response def build_policy(self, request: HttpRequest, response: HttpResponseBase) -> str: warnings.warn("deprecated in favor of get_policy_parts", DeprecationWarning) policy_parts = self.get_policy_parts(request=request, response=response, report_only=False) return build_policy(**asdict(policy_parts)) def build_policy_ro(self, request: HttpRequest, response: HttpResponseBase) -> str: warnings.warn("deprecated in favor of get_policy_parts", DeprecationWarning) policy_parts_ro = self.get_policy_parts(request=request, response=response, report_only=True) return build_policy(**asdict(policy_parts_ro), report_only=True) def get_policy_parts( self, request: HttpRequest, response: HttpResponseBase, report_only: bool = False, ) -> PolicyParts: if report_only: config = getattr(response, "_csp_config_ro", None) update = getattr(response, "_csp_update_ro", None) replace = getattr(response, "_csp_replace_ro", None) else: config = getattr(response, "_csp_config", None) update = getattr(response, "_csp_update", None) replace = getattr(response, "_csp_replace", None) nonce = getattr(request, "_csp_nonce", None) return PolicyParts(config, update, replace, nonce) mozilla-django-csp-6d5cc6c/csp/models.py000066400000000000000000000000461477325434700203450ustar00rootroot00000000000000# This file intentionally left blank. mozilla-django-csp-6d5cc6c/csp/py.typed000066400000000000000000000000001477325434700201750ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/templatetags/000077500000000000000000000000001477325434700212025ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/templatetags/__init__.py000066400000000000000000000000001477325434700233010ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/templatetags/csp.py000066400000000000000000000031361477325434700223440ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from django import template from django.template.base import token_kwargs from csp.utils import build_script_tag if TYPE_CHECKING: from django.template.base import FilterExpression, NodeList, Parser, Token from django.template.context import Context register = template.Library() def _unquote(s: str) -> str: """Helper func that strips single and double quotes from inside strings""" return s.replace('"', "").replace("'", "") @register.tag(name="script") def script(parser: Parser, token: Token) -> NonceScriptNode: # Parse out any keyword args token_args = token.split_contents() kwargs = token_kwargs(token_args[1:], parser) nodelist = parser.parse(("endscript",)) parser.delete_first_token() return NonceScriptNode(nodelist, **kwargs) class NonceScriptNode(template.Node): def __init__(self, nodelist: NodeList, **kwargs: FilterExpression) -> None: self.nodelist = nodelist self.script_attrs = {} for k, v in kwargs.items(): self.script_attrs[k] = self._get_token_value(v) def _get_token_value(self, t: FilterExpression) -> str | None: if hasattr(t, "token") and t.token: return _unquote(t.token) return None def render(self, context: Context) -> str: output = self.nodelist.render(context).strip() request = context.get("request") nonce = str(getattr(request, "csp_nonce", "")) self.script_attrs.update({"nonce": nonce, "content": output}) return build_script_tag(**self.script_attrs) mozilla-django-csp-6d5cc6c/csp/tests/000077500000000000000000000000001477325434700176525ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/tests/__init__.py000066400000000000000000000000001477325434700217510ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/csp/tests/environment.py000066400000000000000000000002311477325434700225640ustar00rootroot00000000000000from typing import Any from jinja2 import Environment def environment(**options: Any) -> Environment: env = Environment(**options) return env mozilla-django-csp-6d5cc6c/csp/tests/settings.py000066400000000000000000000016071477325434700220700ustar00rootroot00000000000000from csp.constants import NONCE, SELF CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "default-src": [SELF, NONCE], } } DATABASES = { "default": { "NAME": "test.db", "ENGINE": "django.db.backends.sqlite3", } } INSTALLED_APPS = ( "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.sites", "csp", ) SECRET_KEY = "csp-test-key" TEMPLATES = [ { "BACKEND": "django.template.backends.jinja2.Jinja2", "DIRS": [], "APP_DIRS": True, "OPTIONS": { "environment": "csp.tests.environment.environment", "extensions": ["csp.extensions.NoncedScript"], }, }, { "BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": [], "APP_DIRS": True, "OPTIONS": {}, }, ] mozilla-django-csp-6d5cc6c/csp/tests/test_checks.py000066400000000000000000000045621477325434700225320ustar00rootroot00000000000000from django.test.utils import override_settings from csp.checks import check_django_csp_lt_4_0, check_exclude_url_prefixes_is_not_string, migrate_settings from csp.constants import NONCE @override_settings( CSP_REPORT_PERCENTAGE=0.25, CSP_EXCLUDE_URL_PREFIXES=["/admin/"], CSP_REPORT_ONLY=False, CSP_DEFAULT_SRC=["'self'", "example.com"], ) def test_migrate_settings() -> None: config, report_only = migrate_settings() assert config == { "REPORT_PERCENTAGE": 25.0, "EXCLUDE_URL_PREFIXES": ["/admin/"], "DIRECTIVES": {"default-src": ["'self'", "example.com"]}, } assert report_only is False @override_settings( CSP_REPORT_ONLY=True, CSP_DEFAULT_SRC=["'self'", "example.com"], CSP_SCRIPT_SRC=["'self'", "example.com", "'unsafe-inline'"], CSP_INCLUDE_NONCE_IN=["script-src"], ) def test_migrate_settings_report_only() -> None: config, report_only = migrate_settings() assert config == { "DIRECTIVES": { "default-src": ["'self'", "example.com"], "script-src": ["'self'", "example.com", "'unsafe-inline'", NONCE], } } assert report_only is True @override_settings( CSP_DEFAULT_SRC=["'self'", "example.com"], ) def test_check_django_csp_lt_4_0() -> None: errors = check_django_csp_lt_4_0(None) assert len(errors) == 1 error = errors[0] assert error.id == "csp.E001" assert "update your settings to use the new format" in error.msg def test_check_django_csp_lt_4_0_no_config() -> None: assert check_django_csp_lt_4_0(None) == [] @override_settings( CONTENT_SECURITY_POLICY={"EXCLUDE_URL_PREFIXES": "/admin/"}, ) def test_check_exclude_url_prefixes_is_not_string() -> None: errors = check_exclude_url_prefixes_is_not_string(None) assert len(errors) == 1 error = errors[0] assert error.id == "csp.E002" assert error.msg == "EXCLUDE_URL_PREFIXES in CONTENT_SECURITY_POLICY settings must be a list or tuple." @override_settings( CONTENT_SECURITY_POLICY_REPORT_ONLY={"EXCLUDE_URL_PREFIXES": "/admin/"}, ) def test_check_exclude_url_prefixes_ro_is_not_string() -> None: errors = check_exclude_url_prefixes_is_not_string(None) assert len(errors) == 1 error = errors[0] assert error.id == "csp.E002" assert error.msg == "EXCLUDE_URL_PREFIXES in CONTENT_SECURITY_POLICY_REPORT_ONLY settings must be a list or tuple." mozilla-django-csp-6d5cc6c/csp/tests/test_constants.py000066400000000000000000000003241477325434700232760ustar00rootroot00000000000000from csp import constants def test_nonce() -> None: assert constants.Nonce() == constants.Nonce() assert constants.NONCE == constants.Nonce() assert repr(constants.Nonce()) == "csp.constants.NONCE" mozilla-django-csp-6d5cc6c/csp/tests/test_context_processors.py000066400000000000000000000022171477325434700252330ustar00rootroot00000000000000from django.http import HttpResponse from django.test import RequestFactory import pytest from csp.context_processors import nonce from csp.exceptions import CSPNonceError from csp.middleware import CSPMiddleware from csp.tests.utils import response rf = RequestFactory() mw = CSPMiddleware(response()) def test_nonce_context_processor() -> None: request = rf.get("/") mw.process_request(request) context = nonce(request) response = HttpResponse() csp_nonce = getattr(request, "csp_nonce") mw.process_response(request, response) assert context["CSP_NONCE"] == csp_nonce def test_nonce_context_processor_after_response() -> None: request = rf.get("/") mw.process_request(request) context = nonce(request) response = HttpResponse() csp_nonce = getattr(request, "csp_nonce") mw.process_response(request, response) assert context["CSP_NONCE"] == csp_nonce with pytest.raises(CSPNonceError): str(getattr(request, "csp_nonce")) def test_nonce_context_processor_with_middleware_disabled() -> None: request = rf.get("/") context = nonce(request) assert context["CSP_NONCE"] == "" mozilla-django-csp-6d5cc6c/csp/tests/test_contrib.py000066400000000000000000000052701477325434700227270ustar00rootroot00000000000000from django.http import HttpResponse from django.test import RequestFactory from django.test.utils import override_settings from csp.constants import HEADER, HEADER_REPORT_ONLY from csp.contrib.rate_limiting import RateLimitedCSPMiddleware from csp.tests.utils import response mw = RateLimitedCSPMiddleware(response()) rf = RequestFactory() @override_settings(CONTENT_SECURITY_POLICY={"REPORT_PERCENTAGE": 10, "DIRECTIVES": {"report-uri": "x"}}) def test_report_percentage() -> None: times_seen = 0 for _ in range(5000): request = rf.get("/") response = HttpResponse() mw.process_response(request, response) if "report-uri" in response[HEADER]: times_seen += 1 if "report-to" in response[HEADER]: times_seen += 1 # Roughly 10% assert 400 <= times_seen <= 600 @override_settings(CONTENT_SECURITY_POLICY={"REPORT_PERCENTAGE": 9.9, "DIRECTIVES": {"report-uri": "x"}}) def test_report_percentage_float() -> None: times_seen = 0 for _ in range(5000): request = rf.get("/") response = HttpResponse() mw.process_response(request, response) if "report-uri" in response[HEADER]: times_seen += 1 if "report-to" in response[HEADER]: times_seen += 1 # Roughly 10% assert 400 <= times_seen <= 600 @override_settings(CONTENT_SECURITY_POLICY={"REPORT_PERCENTAGE": 100, "DIRECTIVES": {"report-uri": "x"}}) def test_report_percentage_100() -> None: times_seen = 0 for _ in range(1000): request = rf.get("/") response = HttpResponse() mw.process_response(request, response) if "report-uri" in response[HEADER]: times_seen += 1 if "report-to" in response[HEADER]: times_seen += 1 assert times_seen == 1000 @override_settings(CONTENT_SECURITY_POLICY_REPORT_ONLY={"REPORT_PERCENTAGE": 10, "DIRECTIVES": {"report-uri": "x"}}) def test_report_percentage_report_only() -> None: times_seen = 0 for _ in range(5000): request = rf.get("/") response = HttpResponse() mw.process_response(request, response) if "report-uri" in response[HEADER_REPORT_ONLY]: times_seen += 1 # Roughly 10% assert 400 <= times_seen <= 600 @override_settings(CONTENT_SECURITY_POLICY=None) def test_no_csp() -> None: request = rf.get("/") response = HttpResponse() mw.process_response(request, response) assert HEADER not in response @override_settings(CONTENT_SECURITY_POLICY_REPORT_ONLY=None) def test_no_csp_ro() -> None: request = rf.get("/") response = HttpResponse() mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response mozilla-django-csp-6d5cc6c/csp/tests/test_decorators.py000066400000000000000000000271751477325434700234440ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING from django.http import HttpResponse from django.test import RequestFactory from django.test.utils import override_settings import pytest from csp.constants import HEADER, HEADER_REPORT_ONLY, NONCE from csp.decorators import csp, csp_exempt, csp_replace, csp_update from csp.middleware import CSPMiddleware from csp.tests.utils import response if TYPE_CHECKING: from django.http import HttpRequest, HttpResponseBase mw = CSPMiddleware(response()) def test_csp_exempt() -> None: @csp_exempt() def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view(RequestFactory().get("/")) assert getattr(response, "_csp_exempt") is True assert not hasattr(response, "_csp_exempt_ro") def test_csp_exempt_ro() -> None: @csp_exempt(REPORT_ONLY=True) def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view(RequestFactory().get("/")) assert not hasattr(response, "_csp_exempt") assert getattr(response, "_csp_exempt_ro") is True @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["foo.com"]}}) def test_csp_update() -> None: request = RequestFactory().get("/") def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_update({"img-src": ["bar.com", NONCE]}) def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) assert getattr(response, "_csp_update") == {"img-src": ["bar.com", NONCE]} mw.process_request(request) csp_nonce = str(getattr(request, "csp_nonce")) # This also triggers the nonce creation. mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'", f"img-src foo.com bar.com 'nonce-{csp_nonce}'"] response = view_without_decorator(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @override_settings(CONTENT_SECURITY_POLICY=None, CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"img-src": ["foo.com"]}}) def test_csp_update_ro() -> None: request = RequestFactory().get("/") def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) mw.process_response(request, response) assert HEADER not in response.headers policy_list = sorted(response[HEADER_REPORT_ONLY].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_update({"img-src": ["bar.com", NONCE]}, REPORT_ONLY=True) def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) assert getattr(response, "_csp_update_ro") == {"img-src": ["bar.com", NONCE]} mw.process_request(request) csp_nonce = str(getattr(request, "csp_nonce")) # This also triggers the nonce creation. mw.process_response(request, response) assert HEADER not in response.headers policy_list = sorted(response[HEADER_REPORT_ONLY].split("; ")) assert policy_list == ["default-src 'self'", f"img-src foo.com bar.com 'nonce-{csp_nonce}'"] response = view_without_decorator(request) mw.process_response(request, response) assert HEADER not in response.headers policy_list = sorted(response[HEADER_REPORT_ONLY].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["foo.com"]}}) def test_csp_replace() -> None: request = RequestFactory().get("/") def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace({"img-src": ["bar.com"]}) def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) assert getattr(response, "_csp_replace") == {"img-src": ["bar.com"]} mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'", "img-src bar.com"] response = view_without_decorator(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace({"img-src": None}) def view_removing_directive(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_removing_directive(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'"] @override_settings(CONTENT_SECURITY_POLICY=None, CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"img-src": ["foo.com"]}}) def test_csp_replace_ro() -> None: request = RequestFactory().get("/") def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) mw.process_response(request, response) assert HEADER not in response.headers policy_list = sorted(response[HEADER_REPORT_ONLY].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace({"img-src": ["bar.com"]}, REPORT_ONLY=True) def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) assert getattr(response, "_csp_replace_ro") == {"img-src": ["bar.com"]} mw.process_response(request, response) assert HEADER not in response.headers policy_list = sorted(response[HEADER_REPORT_ONLY].split("; ")) assert policy_list == ["default-src 'self'", "img-src bar.com"] response = view_without_decorator(request) mw.process_response(request, response) assert HEADER not in response.headers policy_list = sorted(response[HEADER_REPORT_ONLY].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace({"img-src": None}, REPORT_ONLY=True) def view_removing_directive(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_removing_directive(request) mw.process_response(request, response) assert HEADER not in response.headers policy_list = sorted(response[HEADER_REPORT_ONLY].split("; ")) assert policy_list == ["default-src 'self'"] def test_csp() -> None: request = RequestFactory().get("/") def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'"] @csp({"img-src": ["foo.com"], "font-src": ["bar.com"]}) def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) assert getattr(response, "_csp_config") == {"img-src": ["foo.com"], "font-src": ["bar.com"]} mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["font-src bar.com", "img-src foo.com"] response = view_without_decorator(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'"] def test_csp_ro() -> None: request = RequestFactory().get("/") def view_without_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_without_decorator(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'"] @csp({"img-src": ["foo.com"], "font-src": ["bar.com"]}, REPORT_ONLY=True) @csp({}) # CSP with no directives effectively removes the header. def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) assert getattr(response, "_csp_config_ro") == {"img-src": ["foo.com"], "font-src": ["bar.com"]} mw.process_response(request, response) assert HEADER not in response.headers policy_list = sorted(response[HEADER_REPORT_ONLY].split("; ")) assert policy_list == ["font-src bar.com", "img-src foo.com"] response = view_without_decorator(request) mw.process_response(request, response) assert HEADER_REPORT_ONLY not in response.headers policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'"] def test_csp_string_values() -> None: # Test backwards compatibility where values were strings request = RequestFactory().get("/") @csp({"img-src": "foo.com", "font-src": "bar.com"}) def view_with_decorator(request: HttpRequest) -> HttpResponseBase: return HttpResponse() response = view_with_decorator(request) assert getattr(response, "_csp_config") == {"img-src": ["foo.com"], "font-src": ["bar.com"]} mw.process_response(request, response) policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["font-src bar.com", "img-src foo.com"] # Deprecation tests def test_csp_exempt_error() -> None: with pytest.raises(RuntimeError) as excinfo: # Ignore type error since we're checking for the exception raised for 3.x syntax @csp_exempt # type: ignore def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() assert "Incompatible `csp_exempt` decorator usage" in str(excinfo.value) def test_csp_update_error() -> None: with pytest.raises(RuntimeError) as excinfo: @csp_update(IMG_SRC="bar.com") def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() assert "Incompatible `csp_update` decorator arguments" in str(excinfo.value) def test_csp_replace_error() -> None: with pytest.raises(RuntimeError) as excinfo: @csp_replace(IMG_SRC="bar.com") def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() assert "Incompatible `csp_replace` decorator arguments" in str(excinfo.value) def test_csp_error() -> None: with pytest.raises(RuntimeError) as excinfo: @csp(IMG_SRC=["bar.com"]) def view(request: HttpRequest) -> HttpResponseBase: return HttpResponse() assert "Incompatible `csp` decorator arguments" in str(excinfo.value) mozilla-django-csp-6d5cc6c/csp/tests/test_jinja_extension.py000066400000000000000000000057051477325434700244610ustar00rootroot00000000000000from csp.tests.utils import ScriptExtensionTestBase class TestJinjaExtension(ScriptExtensionTestBase): def test_script_tag_injects_nonce(self) -> None: tpl = """ {% script %} var hello='world'; {% endscript %} """ expected = """""" self.assert_template_eq(*self.process_templates(tpl, expected)) def test_script_with_src_ignores_body(self) -> None: tpl = """ {% script src="foo" %} var hello='world'; {% endscript %} """ expected = """""" self.assert_template_eq(*self.process_templates(tpl, expected)) def test_script_tag_sets_attrs_correctly(self) -> None: tpl = """ {% script id='jeff' defer=True %} var hello='world'; {% endscript %} """ expected = """ """ self.assert_template_eq(*self.process_templates(tpl, expected)) def test_async_attribute_with_falsey(self) -> None: tpl = """ {% script id="jeff" async=False %} var hello='world'; {% endscript %}""" expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) def test_async_attribute_with_truthy(self) -> None: tpl = """ {% script id="jeff" async=True %} var hello='world'; {% endscript %}""" expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) def test_nested_script_tags_are_removed(self) -> None: """Let users wrap their code in script tags for the sake of their development environment""" tpl = """ {% script type="application/javascript" id="jeff" defer=True%} {% endscript %}""" expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) def test_regex_captures_script_content_including_brackets(self) -> None: """ Ensure that script content get captured properly. Especially when using angle brackets.""" tpl = """ {% script %} " {% endscript %} """ expected = '"' self.assert_template_eq(*self.process_templates(tpl, expected)) mozilla-django-csp-6d5cc6c/csp/tests/test_middleware.py000066400000000000000000000217431477325434700234070ustar00rootroot00000000000000from django.http import ( HttpResponse, HttpResponseNotFound, HttpResponseServerError, ) from django.template import Context, Template, engines from django.test import RequestFactory from django.test.utils import override_settings import pytest from csp.constants import HEADER, HEADER_REPORT_ONLY, SELF from csp.exceptions import CSPNonceError from csp.middleware import CheckableLazyObject, CSPMiddleware from csp.tests.utils import response mw = CSPMiddleware(response()) rf = RequestFactory() def test_checkable_lazy_object() -> None: def generate_value() -> str: return "generated" lazy = CheckableLazyObject(generate_value) # Before wrapped object is initiated, lazy is falsy assert bool(lazy) is False # After str(lazy) calls generate_value, lazy is truthy assert str(lazy) == "generated" assert bool(lazy) is True def test_add_header() -> None: request = rf.get("/") response = HttpResponse() mw.process_response(request, response) assert HEADER in response @override_settings( CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["example.com"]}}, CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": [SELF]}}, ) def test_both_headers() -> None: request = rf.get("/") response = HttpResponse() mw.process_response(request, response) assert HEADER in response assert HEADER_REPORT_ONLY in response @override_settings( CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": {"example.com"}}}, CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": {SELF}}}, ) def test_directives_configured_as_sets() -> None: request = rf.get("/") response = HttpResponse() mw.process_response(request, response) assert HEADER in response assert HEADER_REPORT_ONLY in response def test_exempt() -> None: request = rf.get("/") response = HttpResponse() setattr(response, "_csp_exempt", True) mw.process_response(request, response) assert HEADER not in response @override_settings(CONTENT_SECURITY_POLICY={"EXCLUDE_URL_PREFIXES": ["/inlines-r-us"]}) def test_exclude() -> None: request = rf.get("/inlines-r-us/foo") response = HttpResponse() mw.process_response(request, response) assert HEADER not in response @override_settings( CONTENT_SECURITY_POLICY=None, CONTENT_SECURITY_POLICY_REPORT_ONLY={"DIRECTIVES": {"default-src": [SELF]}}, ) def test_report_only() -> None: request = rf.get("/") response = HttpResponse() mw.process_response(request, response) assert HEADER not in response assert HEADER_REPORT_ONLY in response assert response[HEADER_REPORT_ONLY] == "default-src 'self'" def test_dont_replace() -> None: request = rf.get("/") response = HttpResponse() response[HEADER] = "default-src example.com" mw.process_response(request, response) assert response[HEADER] == "default-src example.com" def test_use_config() -> None: request = rf.get("/") response = HttpResponse() setattr(response, "_csp_config", {"default-src": ["example.com"]}) mw.process_response(request, response) assert response[HEADER] == "default-src example.com" def test_use_update() -> None: request = rf.get("/") response = HttpResponse() setattr(response, "_csp_update", {"default-src": ["example.com"]}) mw.process_response(request, response) assert response[HEADER] == "default-src 'self' example.com" @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["foo.com"]}}) def test_use_replace() -> None: request = rf.get("/") response = HttpResponse() setattr(response, "_csp_replace", {"img-src": ["bar.com"]}) mw.process_response(request, response) policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'", "img-src bar.com"] @override_settings(DEBUG=True) def test_debug_errors_exempt() -> None: request = rf.get("/") response = HttpResponseServerError() mw.process_response(request, response) assert HEADER not in response @override_settings(DEBUG=True) def test_debug_notfound_exempt() -> None: request = rf.get("/") response = HttpResponseNotFound() mw.process_response(request, response) assert HEADER not in response def test_nonce_created_when_accessed() -> None: request = rf.get("/") mw.process_request(request) nonce = str(getattr(request, "csp_nonce")) response = HttpResponse() mw.process_response(request, response) assert nonce in response[HEADER] assert response[HEADER] == f"default-src 'self' 'nonce-{nonce}'" def test_nonce_is_false_before_access_and_true_after() -> None: request = rf.get("/") mw.process_request(request) assert bool(getattr(request, "csp_nonce")) is False nonce = str(getattr(request, "csp_nonce")) assert bool(getattr(request, "csp_nonce")) is True response = HttpResponse() mw.process_response(request, response) assert bool(getattr(request, "csp_nonce")) is True assert getattr(request, "csp_nonce") == nonce def test_nonce_conditional_in_django_template() -> None: """An unset nonce is Falsy in a template context""" template = Template( """ {% if request.csp_nonce %} The CSP nonce is {{ request.csp_nonce }}. {% else %} The CSP nonce is not set. {% endif %} """ ) request = rf.get("/") context = Context({"request": request}) mw.process_request(request) rendered_unset = template.render(context).strip() assert rendered_unset == "The CSP nonce is not set." nonce = str(getattr(request, "csp_nonce")) rendered_set = template.render(context).strip() assert rendered_set == f"The CSP nonce is {nonce}." def test_nonce_usage_in_django_template() -> None: """Reading a nonce in a template context generates the nonce""" template = Template("The CSP nonce is {{ request.csp_nonce }}.") request = rf.get("/") context = Context({"request": request}) mw.process_request(request) nonce = getattr(request, "csp_nonce", None) assert bool(nonce) is False rendered = template.render(context) assert bool(nonce) is True assert rendered == f"The CSP nonce is {nonce}." def test_nonce_conditional_in_jinja2_template() -> None: """An unset nonce is Falsy in a template context""" template = engines["jinja2"].from_string( """ {% if request.csp_nonce %} The CSP nonce is {{ request.csp_nonce }}. {% else %} The CSP nonce is not set. {% endif %} """ ) request = rf.get("/") context = {"request": request} mw.process_request(request) rendered_unset = template.render(context).strip() assert rendered_unset == "The CSP nonce is not set." nonce = str(getattr(request, "csp_nonce")) rendered_set = template.render(context).strip() assert rendered_set == f"The CSP nonce is {nonce}." def test_nonce_usage_in_jinja2_template() -> None: """Reading a nonce in a template context generates the nonce""" template = engines["jinja2"].from_string("The CSP nonce is {{ request.csp_nonce }}.") request = rf.get("/") context = {"request": request} mw.process_request(request) nonce = getattr(request, "csp_nonce", None) assert bool(nonce) is False rendered = template.render(context) assert bool(nonce) is True assert rendered == f"The CSP nonce is {nonce}." def test_no_nonce_when_not_accessed() -> None: request = rf.get("/") mw.process_request(request) response = HttpResponse() mw.process_response(request, response) assert "nonce-" not in response[HEADER] assert response[HEADER] == "default-src 'self'" def test_nonce_regenerated_on_new_request() -> None: request1 = rf.get("/") request2 = rf.get("/") mw.process_request(request1) mw.process_request(request2) nonce1 = str(getattr(request1, "csp_nonce")) nonce2 = str(getattr(request2, "csp_nonce")) assert nonce1 != nonce2 response1 = HttpResponse() response2 = HttpResponse() mw.process_response(request1, response1) mw.process_response(request2, response2) assert nonce1 not in response2[HEADER] assert nonce2 not in response1[HEADER] def test_no_nonce_access_after_middleware_is_attribute_error() -> None: # Test `CSPNonceError` is raised when accessing an unset nonce after the response has been processed. request = rf.get("/") mw.process_request(request) mw.process_response(request, HttpResponse()) assert bool(getattr(request, "csp_nonce", True)) is False with pytest.raises(CSPNonceError): str(getattr(request, "csp_nonce")) def test_set_nonce_access_after_middleware_is_ok() -> None: # Test accessing a set nonce after the response has been processed is OK. request = rf.get("/") mw.process_request(request) nonce = str(getattr(request, "csp_nonce")) mw.process_response(request, HttpResponse()) assert bool(getattr(request, "csp_nonce", False)) is True assert str(getattr(request, "csp_nonce")) == nonce mozilla-django-csp-6d5cc6c/csp/tests/test_templatetags.py000066400000000000000000000060621477325434700237610ustar00rootroot00000000000000from csp.tests.utils import ScriptTagTestBase class TestDjangoTemplateTag(ScriptTagTestBase): def test_script_tag_injects_nonce(self) -> None: tpl = """ {% load csp %} {% script %}var hello='world';{% endscript %}""" expected = """""" self.assert_template_eq(*self.process_templates(tpl, expected)) def test_script_with_src_ignores_body(self) -> None: tpl = """ {% load csp %} {% script src="foo" %} var hello='world'; {% endscript %}""" expected = """""" self.assert_template_eq(*self.process_templates(tpl, expected)) def test_script_tag_sets_attrs_correctly(self) -> None: tpl = """ {% load csp %} {% script type="application/javascript" id="jeff" defer=True%} var hello='world'; {% endscript %}""" expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) def test_async_attribute_with_falsey(self) -> None: tpl = """ {% load csp %} {% script src="foo.com/bar.js" async=False %} {% endscript %}""" expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) def test_async_attribute_with_truthy(self) -> None: tpl = """ {% load csp %} {% script src="foo.com/bar.js" async=True %} var hello='world'; {% endscript %}""" expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) def test_nested_script_tags_are_removed(self) -> None: """Lets end users wrap their code in script tags for the sake of their development environment""" tpl = """ {% load csp %} {% script type="application/javascript" id="jeff" defer=True%} {% endscript %}""" expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) def test_regex_captures_script_content_including_brackets(self) -> None: """ Ensure that script content get captured properly. Especially when using angle brackets.""" tpl = """ {% load csp %} {% script %} " {% endscript %} """ expected = '"' self.assert_template_eq(*self.process_templates(tpl, expected)) mozilla-django-csp-6d5cc6c/csp/tests/test_utils.py000066400000000000000000000331371477325434700224320ustar00rootroot00000000000000from django.test.utils import override_settings from django.utils.functional import lazy from csp.constants import NONCE, NONE, SELF from csp.utils import DEFAULT_DIRECTIVES, build_policy, default_config def policy_eq(a: str, b: str) -> None: parts_a = sorted(a.split("; ")) parts_b = sorted(b.split("; ")) assert parts_a == parts_b, f"{a!r} != {b!r}" def literal(s: str) -> str: return s lazy_literal = lazy(literal, str) def test_default_config_none() -> None: assert default_config(None) is None def test_default_config_empty() -> None: # Test `default_config` with an empty dict returns defaults. assert default_config({}) == DEFAULT_DIRECTIVES def test_default_config_drops_unknown() -> None: # Test `default_config` drops unknown keys. config = {"foo-src": ["example.com"]} assert default_config(config) == DEFAULT_DIRECTIVES def test_default_config() -> None: # Test `default_config` keeps config along with defaults. config = {"img-src": ["example.com"]} assert default_config(config) == {**DEFAULT_DIRECTIVES, **config} def test_empty_policy() -> None: policy = build_policy() policy_eq("default-src 'self'", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": None}}) def test_default_src_none() -> None: policy = build_policy() policy_eq("", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": ["example.com", "example2.com"]}}) def test_default_src() -> None: policy = build_policy() policy_eq("default-src example.com example2.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": {"example.com", "example2.com"}}}) def test_default_src_is_set() -> None: policy = build_policy() policy_eq("default-src example.com example2.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"script-src": ["example.com"]}}) def test_script_src() -> None: policy = build_policy() policy_eq("default-src 'self'; script-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"script-src-attr": ["example.com"]}}) def test_script_src_attr() -> None: policy = build_policy() policy_eq("default-src 'self'; script-src-attr example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"script-src-elem": ["example.com"]}}) def test_script_src_elem() -> None: policy = build_policy() policy_eq("default-src 'self'; script-src-elem example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"object-src": ["example.com"]}}) def test_object_src() -> None: policy = build_policy() policy_eq("default-src 'self'; object-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"prefetch-src": ["example.com"]}}) def test_prefetch_src() -> None: policy = build_policy() policy_eq("default-src 'self'; prefetch-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"style-src": ["example.com"]}}) def test_style_src() -> None: policy = build_policy() policy_eq("default-src 'self'; style-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"style-src-attr": ["example.com"]}}) def test_style_src_attr() -> None: policy = build_policy() policy_eq("default-src 'self'; style-src-attr example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"style-src-elem": ["example.com"]}}) def test_style_src_elem() -> None: policy = build_policy() policy_eq("default-src 'self'; style-src-elem example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["example.com"]}}) def test_img_src() -> None: policy = build_policy() policy_eq("default-src 'self'; img-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"media-src": ["example.com"]}}) def test_media_src() -> None: policy = build_policy() policy_eq("default-src 'self'; media-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"frame-src": ["example.com"]}}) def test_frame_src() -> None: policy = build_policy() policy_eq("default-src 'self'; frame-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"font-src": ["example.com"]}}) def test_font_src() -> None: policy = build_policy() policy_eq("default-src 'self'; font-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"connect-src": ["example.com"]}}) def test_connect_src() -> None: policy = build_policy() policy_eq("default-src 'self'; connect-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"sandbox": ["allow-scripts"]}}) def test_sandbox() -> None: policy = build_policy() policy_eq("default-src 'self'; sandbox allow-scripts", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"sandbox": []}}) def test_sandbox_empty() -> None: policy = build_policy() policy_eq("default-src 'self'; sandbox", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"report-uri": "/foo"}}) def test_report_uri() -> None: policy = build_policy() policy_eq("default-src 'self'; report-uri /foo", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"report-uri": lazy_literal("/foo")}}) def test_report_uri_lazy() -> None: policy = build_policy() policy_eq("default-src 'self'; report-uri /foo", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"report-to": "some_endpoint"}}) def test_report_to() -> None: policy = build_policy() policy_eq("default-src 'self'; report-to some_endpoint", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["example.com"]}}) def test_update_img() -> None: policy = build_policy(update={"img-src": "example2.com"}) policy_eq("default-src 'self'; img-src example.com example2.com", policy) def test_update_missing_setting() -> None: """update should work even if the setting is not defined.""" policy = build_policy(update={"img-src": "example.com"}) policy_eq("default-src 'self'; img-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["example.com"]}}) def test_replace_img() -> None: policy = build_policy(replace={"img-src": "example2.com"}) policy_eq("default-src 'self'; img-src example2.com", policy) def test_replace_missing_setting() -> None: """replace should work even if the setting is not defined.""" policy = build_policy(replace={"img-src": "example.com"}) policy_eq("default-src 'self'; img-src example.com", policy) def test_config() -> None: policy = build_policy(config={"default-src": [NONE], "img-src": [SELF]}) policy_eq("default-src 'none'; img-src 'self'", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ("example.com",)}}) def test_update_string() -> None: """ GitHub issue #40 - given project settings as a tuple, and an update/replace with a string, concatenate correctly. """ policy = build_policy(update={"img-src": "example2.com"}) policy_eq("default-src 'self'; img-src example.com example2.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ("example.com",)}}) def test_replace_string() -> None: """ Demonstrate that GitHub issue #40 doesn't affect replacements """ policy = build_policy(replace={"img-src": "example2.com"}) policy_eq("default-src 'self'; img-src example2.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ("example.com",)}}) def test_update_set() -> None: """ GitHub issue #40 - given project settings as a tuple, and an update/replace with a string, concatenate correctly. """ policy = build_policy(update={"img-src": {"example2.com"}}) policy_eq("default-src 'self'; img-src example.com example2.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ("example.com",)}}) def test_replace_set() -> None: """ Demonstrate that GitHub issue #40 doesn't affect replacements """ policy = build_policy(replace={"img-src": {"example2.com"}}) policy_eq("default-src 'self'; img-src example2.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"form-action": ["example.com"]}}) def test_form_action() -> None: policy = build_policy() policy_eq("default-src 'self'; form-action example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"base-uri": ["example.com"]}}) def test_base_uri() -> None: policy = build_policy() policy_eq("default-src 'self'; base-uri example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"child-src": ["example.com"]}}) def test_child_src() -> None: policy = build_policy() policy_eq("default-src 'self'; child-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"frame-ancestors": ["example.com"]}}) def test_frame_ancestors() -> None: policy = build_policy() policy_eq("default-src 'self'; frame-ancestors example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"navigate-to": ["example.com"]}}) def test_navigate_to() -> None: policy = build_policy() policy_eq("default-src 'self'; navigate-to example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"manifest-src": ["example.com"]}}) def test_manifest_src() -> None: policy = build_policy() policy_eq("default-src 'self'; manifest-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"worker-src": ["example.com"]}}) def test_worker_src() -> None: policy = build_policy() policy_eq("default-src 'self'; worker-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"plugin-types": ["application/pdf"]}}) def test_plugin_types() -> None: policy = build_policy() policy_eq("default-src 'self'; plugin-types application/pdf", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"require-sri-for": ["script"]}}) def test_require_sri_for() -> None: policy = build_policy() policy_eq("default-src 'self'; require-sri-for script", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"require-trusted-types-for": ["'script'"]}}) def test_require_trusted_types_for() -> None: policy = build_policy() policy_eq("default-src 'self'; require-trusted-types-for 'script'", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"trusted-types": ["strictPolicy", "laxPolicy", "default", "'allow-duplicates'"]}}) def test_trusted_types() -> None: policy = build_policy() policy_eq( "default-src 'self'; trusted-types strictPolicy laxPolicy default 'allow-duplicates'", policy, ) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"upgrade-insecure-requests": True}}) def test_upgrade_insecure_requests() -> None: policy = build_policy() policy_eq("default-src 'self'; upgrade-insecure-requests", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"block-all-mixed-content": True}}) def test_block_all_mixed_content() -> None: policy = build_policy() policy_eq("default-src 'self'; block-all-mixed-content", policy) def test_nonce() -> None: policy = build_policy(nonce="abc123") policy_eq("default-src 'self' 'nonce-abc123'", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": [SELF], "script-src": [SELF, NONCE], "style-src": [SELF, NONCE]}}) def test_nonce_in_value() -> None: policy = build_policy(nonce="abc123") policy_eq( "default-src 'self'; script-src 'self' 'nonce-abc123'; style-src 'self' 'nonce-abc123'", policy, ) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"default-src": [NONCE]}}) def test_only_nonce_in_value() -> None: policy = build_policy(nonce="abc123") policy_eq("default-src 'nonce-abc123'", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["example.com", "example.com"]}}) def test_deduplicate_values() -> None: """ GitHub issue #40 - given project settings as a tuple, and an update/replace with a string, concatenate correctly. """ policy = build_policy() policy_eq("default-src 'self'; img-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ["example.com", "example.com"]}}) def test_deduplicate_values_update() -> None: """ GitHub issue #40 - given project settings as a tuple, and an update/replace with a string, concatenate correctly. """ policy = build_policy(update={"img-src": "example.com"}) policy_eq("default-src 'self'; img-src example.com", policy) @override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {"img-src": ("example.com",)}}) def test_deduplicate_values_replace() -> None: """ Demonstrate that GitHub issue #40 doesn't affect replacements """ policy = build_policy(replace={"img-src": ["example2.com", "example2.com"]}) policy_eq("default-src 'self'; img-src example2.com", policy) def test_boolean_directives() -> None: for directive in ["upgrade-insecure-requests", "block-all-mixed-content"]: with override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {directive: True}}): policy = build_policy() policy_eq(f"default-src 'self'; {directive}", policy) with override_settings(CONTENT_SECURITY_POLICY={"DIRECTIVES": {directive: False}}): policy = build_policy() policy_eq("default-src 'self'", policy) mozilla-django-csp-6d5cc6c/csp/tests/utils.py000066400000000000000000000037671477325434700214010ustar00rootroot00000000000000from __future__ import annotations from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Callable from django.http import HttpResponse from django.template import Context, Template, engines from django.test import RequestFactory from django.utils.functional import SimpleLazyObject from csp.middleware import CSPMiddleware if TYPE_CHECKING: from django.http import HttpRequest def response(*args: Any, headers: dict[str, str] | None = None, **kwargs: Any) -> Callable[[HttpRequest], HttpResponse]: def get_response(req: HttpRequest) -> HttpResponse: response = HttpResponse(*args, **kwargs) if headers: for k, v in headers.items(): response.headers[k] = v return response return get_response JINJA_ENV = engines["jinja2"] mw = CSPMiddleware(response()) rf = RequestFactory() class ScriptTestBase(ABC): def assert_template_eq(self, tpl1: str, tpl2: str) -> None: aaa = tpl1.replace("\n", "").replace(" ", "") bbb = tpl2.replace("\n", "").replace(" ", "") assert aaa == bbb, f"{aaa} != {bbb}" def process_templates(self, tpl: str, expected: str) -> tuple[str, str]: request = rf.get("/") mw.process_request(request) nonce = getattr(request, "csp_nonce") assert isinstance(nonce, SimpleLazyObject) return (self.render(tpl, request).strip(), expected.format(nonce)) @abstractmethod def render(self, template_string: str, request: HttpRequest) -> str: ... class ScriptTagTestBase(ScriptTestBase): def render(self, template_string: str, request: HttpRequest) -> str: context = Context({"request": request}) template = Template(template_string) return template.render(context) class ScriptExtensionTestBase(ScriptTestBase): def render(self, template_string: str, request: HttpRequest) -> str: context = {"request": request} template = JINJA_ENV.from_string(template_string) return template.render(context) mozilla-django-csp-6d5cc6c/csp/utils.py000066400000000000000000000146721477325434700202340ustar00rootroot00000000000000from __future__ import annotations import copy import re from itertools import chain from typing import Any, Callable from django.conf import settings from django.utils.encoding import force_str from csp.constants import NONCE, SELF DEFAULT_DIRECTIVES = { # Fetch Directives "child-src": None, "connect-src": None, "default-src": [SELF], "script-src": None, "script-src-attr": None, "script-src-elem": None, "object-src": None, "style-src": None, "style-src-attr": None, "style-src-elem": None, "font-src": None, "frame-src": None, "img-src": None, "manifest-src": None, "media-src": None, "prefetch-src": None, # Deprecated. # Document Directives "base-uri": None, "plugin-types": None, # Deprecated. "sandbox": None, # Navigation Directives "form-action": None, "frame-ancestors": None, "navigate-to": None, # Reporting Directives "report-uri": None, "report-to": None, "require-sri-for": None, # Trusted Types Directives "require-trusted-types-for": None, "trusted-types": None, # Other Directives "webrtc": None, "worker-src": None, # Directives Defined in Other Documents "upgrade-insecure-requests": False, "block-all-mixed-content": False, # Deprecated. } DIRECTIVES_T = dict[str, Any] def default_config(csp: DIRECTIVES_T | None) -> DIRECTIVES_T | None: if csp is None: return None # Make a copy of the passed in config to avoid mutating it, and also to drop any unknown keys. config = {} for key, value in DEFAULT_DIRECTIVES.items(): config[key] = csp.get(key, value) return config def build_policy( config: DIRECTIVES_T | None = None, update: DIRECTIVES_T | None = None, replace: DIRECTIVES_T | None = None, nonce: str | None = None, report_only: bool = False, ) -> str: """Builds the policy as a string from the settings.""" if config is None: if report_only: config = getattr(settings, "CONTENT_SECURITY_POLICY_REPORT_ONLY", {}) config = default_config(config.get("DIRECTIVES", {})) if config else None else: config = getattr(settings, "CONTENT_SECURITY_POLICY", {}) config = default_config(config.get("DIRECTIVES", {})) if config else None # If config is still `None`, return empty policy. if config is None: return "" update = update if update is not None else {} replace = replace if replace is not None else {} csp = {} for k in set(chain(config, replace)): if k in replace: v = replace[k] else: v = config[k] if v is not None: v = copy.copy(v) if isinstance(v, set): v = sorted(v) if not isinstance(v, (list, tuple)): v = (v,) csp[k] = v for k, v in update.items(): if v is not None: v = copy.copy(v) if isinstance(v, set): v = sorted(v) if not isinstance(v, (list, tuple)): v = (v,) if csp.get(k) is None: csp[k] = v else: csp[k] += tuple(v) report_uri = csp.pop("report-uri", None) policy_parts = {} for key, value in csp.items(): # Check for boolean directives. if len(value) == 1 and isinstance(value[0], bool): if value[0] is True: policy_parts[key] = "" continue if NONCE in value: if nonce: value = [f"'nonce-{nonce}'" if v == NONCE else v for v in value] else: # Strip the `NONCE` sentinel value if no nonce is provided. value = [v for v in value if v != NONCE] value = list(dict.fromkeys(value)) # Deduplicate policy_parts[key] = " ".join(value) if report_uri: report_uri = map(force_str, report_uri) policy_parts["report-uri"] = " ".join(report_uri) return "; ".join([f"{k} {val}".strip() for k, val in policy_parts.items()]) def _default_attr_mapper(attr_name: str, val: str) -> str: if val: return f' {attr_name}="{val}"' else: return "" def _bool_attr_mapper(attr_name: str, val: bool) -> str: # Only return the bare word if the value is truthy # ie - defer=False should actually return an empty string if val: return f" {attr_name}" else: return "" def _async_attr_mapper(attr_name: str, val: str | bool) -> str: """The `async` attribute works slightly different than the other bool attributes. It can be set explicitly to `false` with no surrounding quotes according to the spec.""" if val in [False, "False"]: return f" {attr_name}=false" elif val: return f" {attr_name}" else: return "" # Allow per-attribute customization of returned string template SCRIPT_ATTRS: dict[str, Callable[[str, Any], str]] = { "nonce": _default_attr_mapper, "id": _default_attr_mapper, "src": _default_attr_mapper, "type": _default_attr_mapper, "async": _async_attr_mapper, "defer": _bool_attr_mapper, "integrity": _default_attr_mapper, "nomodule": _bool_attr_mapper, } # Generates an interpolatable string of valid attrs eg - '{nonce}{id}...' ATTR_FORMAT_STR = "".join([f"{{{a}}}" for a in SCRIPT_ATTRS]) _script_tag_contents_re = re.compile( r""" # match the closing script tag """, re.VERBOSE, ) def _unwrap_script(text: str) -> str: """Extract content defined between script tags""" matches = re.search(_script_tag_contents_re, text) if matches and len(matches.groups()): return matches.group(1).strip() return text def build_script_tag(content: str | None = None, **kwargs: Any) -> str: data = {} # Iterate all possible script attrs instead of kwargs to make # interpolation as easy as possible below for attr_name, mapper in SCRIPT_ATTRS.items(): data[attr_name] = mapper(attr_name, kwargs.get(attr_name)) # Don't render block contents if the script has a 'src' attribute c = _unwrap_script(content) if content and not kwargs.get("src") else "" attrs = ATTR_FORMAT_STR.format(**data).rstrip() return f"{c}".strip() mozilla-django-csp-6d5cc6c/docs/000077500000000000000000000000001477325434700166535ustar00rootroot00000000000000mozilla-django-csp-6d5cc6c/docs/Makefile000066400000000000000000000127141477325434700203200ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Django-CSP.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Django-CSP.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Django-CSP" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Django-CSP" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." mozilla-django-csp-6d5cc6c/docs/conf.py000066400000000000000000000176261477325434700201660ustar00rootroot00000000000000# # Django-CSP documentation build configuration file, created by # sphinx-quickstart on Wed Oct 31 13:02:27 2012. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. from importlib.metadata import version as get_version from typing import Dict # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx_rtd_theme", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Django-CSP" copyright = "2016-%Y Mozilla" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = get_version("django_csp") # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "Django-CSPdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements: Dict[str, str] = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "index", "Django-CSP.tex", "Django-CSP Documentation", "James Socol, Mozilla", "manual", ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "django-csp", "Django-CSP Documentation", ["James Socol, Mozilla"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( "index", "Django-CSP", "Django-CSP Documentation", "James Socol, Mozilla", "Django-CSP", "CSP header-generation and reporting for Django applications", "Miscellaneous", ), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' mozilla-django-csp-6d5cc6c/docs/configuration.rst000066400000000000000000000315311477325434700222570ustar00rootroot00000000000000.. _configuration-chapter: ====================== Configuring django-csp ====================== Content-Security-Policy_ is a complicated header. There are many values you may need to tweak here. It's worth reading the latest CSP spec_ and making sure you understand it before configuring django-csp. .. note:: Many settings require a ``tuple`` or ``list``. You may get very strange policies and even errors when mistakenly configuring them as a ``string``. Migrating from django-csp <= 3.8 ================================ Version 4.0 of django-csp introduces a new configuration format that breaks compatibility with previous versions. If you are migrating from django-csp 3.8 or lower, you will need to update your settings to the new format. See the :ref:`migration guide ` for more information. Configuration ============= All configuration of django-csp is done in your Django settings file with the ``CONTENT_SECURITY_POLICY`` setting or the ``CONTENT_SECURITY_POLICY_REPORT_ONLY`` setting. Each of these settings expects a dictionary representing a policy. The ``CONTENT_SECURITY_POLICY`` setting is your enforceable policy. The ``CONTENT_SECURITY_POLICY_REPORT_ONLY`` setting is your report-only policy. This policy is used to test the policy without breaking the site. It is useful when setting this policy to be slightly more strict than the default policy to see what would be blocked if the policy was enforced. The following is an example of a policy configuration with a default policy and a report-only policy. The default policy is considered a "relaxed" policy that allows for the most flexibility while still providing a good level of security. The report-only policy is considered a step towards a more slightly strict policy and is used to test the policy without breaking the site. .. code-block:: python from csp.constants import NONE, SELF CONTENT_SECURITY_POLICY = { "EXCLUDE_URL_PREFIXES": ["/excluded-path/"], "DIRECTIVES": { "default-src": [SELF, "cdn.example.net"], "frame-ancestors": [SELF], "form-action": [SELF], "report-uri": "/csp-report/", }, } CONTENT_SECURITY_POLICY_REPORT_ONLY = { "EXCLUDE_URL_PREFIXES": ["/excluded-path/"], "DIRECTIVES": { "default-src": [NONE], "connect-src": [SELF], "img-src": [SELF], "form-action": [SELF], "frame-ancestors": [SELF], "script-src": [SELF], "style-src": [SELF], "upgrade-insecure-requests": True, "report-uri": "/csp-report/", }, } .. note:: In the above example, the constant ``NONE`` is converted to the CSP keyword ``"'none'"`` and is distinct from Python's ``None`` value. The CSP keyword ``'none'`` is a special value that signifies that you do not want any sources for this directive. The ``None`` value is a Python keyword that represents the absence of a value and when used as the value of a directive, it will remove the directive from the policy. This is useful when using the ``@csp_replace`` decorator to effectively clear a directive from the base configuration as defined in the settings. For example, if the Django settings the ``frame-ancestors`` directive is set to a list of sources and you want to remove the ``frame-ancestors`` directive from the policy for this view: .. code-block:: python from csp.decorators import csp_replace @csp_replace({"frame-ancestors": None}) def my_view(request): ... Policy Settings =============== At the top level of the policy dictionary, these are the keys that can be used to configure the policy. ``EXCLUDE_URL_PREFIXES`` A ``tuple`` or ``list`` of URL prefixes to exclude from CSP protection. URLs beginning with any of these strings will not get the Content-Security-Policy response headers at all. .. warning:: Excluding any path on your site will eliminate the benefits of CSP everywhere on your site. The typical browser security model for JavaScript considers all paths alike. A Cross-Site Scripting flaw on, e.g., ``excluded-page/`` can therefore be leveraged to access everything on the same origin. ``REPORT_PERCENTAGE`` Percentage of requests that should see the ``report-uri`` directive. Use this to throttle the number of CSP violation reports made to your ``report-uri``. A **float** between 0.0 and 100.0 (0.0 = no reports at all, 100.0 = always report). Ignored if ``report-uri`` isn't set. .. note:: To allow rate limiting, ``csp.contrib.rate_limiting.RateLimitedCSPMiddleware`` must be used instead of ``csp.middleware.CSPMiddleware``. See :ref:`violation reporting ` for more details. ``DIRECTIVES`` A dictionary of policy directives. Each key in the dictionary is a directive and the value is a list of sources for that directive. The following is a list of all the directives that can be configured. .. note:: The CSP keyword values of ``'self'``, ``'unsafe-inline'``, ``'strict-dynamic'``, etc. must be quoted! e.g.: ``"default-src": ["'self'"]``. Without quotes they will not work as intended. New in version 4.0 are CSP keyword constants. Use these to minimize quoting mistakes and typos. The following CSP keywords are available: * ``NONE`` = ``"'none'"`` * ``REPORT_SAMPLE`` = ``"'report-sample'"`` * ``SELF`` = ``"'self'"`` * ``STRICT_DYNAMIC`` = ``"'strict-dynamic'"`` * ``UNSAFE_ALLOW_REDIRECTS`` = ``"'unsafe-allow-redirects'"`` * ``UNSAFE_EVAL`` = ``"'unsafe-eval'"`` * ``UNSAFE_HASHES`` = ``"'unsafe-hashes'"`` * ``UNSAFE_INLINE`` = ``"'unsafe-inline'"`` * ``WASM_UNSAFE_EVAL`` = ``"'wasm-unsafe-eval'"`` Example usage: .. code-block:: python from csp.constants import NONE, SELF, STRICT_DYNAMIC CONTENT_SECURITY_POLICY = { "DIRECTIVES": { # No sources allowed for default-src by using `csp.constants.NONE`. "default-src": [NONE], "script-src": [SELF, STRICT_DYNAMIC], "style-src": [SELF], # Using Python's `None` will not include the directive in the header. Useful # to override previous settings or when using the decorators. "base-uri": None, } } .. note:: The CSP keyword ``csp.constants.NONE`` is distinct from Python's ``None`` value. The CSP keyword ``'none'`` is a special value that signifies that you do not want any sources for the directive. The ``None`` value is a Python keyword that represents the absence of a value and when used as the value of a directive, it will remove the directive from the header. .. note:: Deprecated features of CSP in general have been moved to the bottom of this list. .. warning:: The ``'unsafe-inline'`` and ``'unsafe-eval'`` sources are considered harmful and should be avoided. They are included here for completeness, but should not be used in production. ``default-src`` Set the ``default-src`` directive. A ``tuple`` or ``list`` of values, e.g.: ``("'self'", "cdn.example.net")``. *default=["'self'"]* ``script-src`` Set the ``script-src`` directive. A ``tuple`` or ``list``. *default=None* ``script-src-attr`` Set the ``script-src-attr`` directive. A ``tuple`` or ``list``. *default=None* ``script-src-elem`` Set the ``script-src-elem`` directive. A ``tuple`` or ``list``. *default=None* ``img-src`` Set the ``img-src`` directive. A ``tuple`` or ``list``. *default=None* ``object-src`` Set the ``object-src`` directive. A ``tuple`` or ``list``. *default=None* ``media-src`` Set the ``media-src`` directive. A ``tuple`` or ``list``. *default=None* ``frame-src`` Set the ``frame-src`` directive. A ``tuple`` or ``list``. *default=None* ``font-src`` Set the ``font-src`` directive. A ``tuple`` or ``list``. *default=None* ``connect-src`` Set the ``connect-src`` directive. A ``tuple`` or ``list``. *default=None* ``style-src`` Set the ``style-src`` directive. A ``tuple`` or ``list``. *default=None* ``style-src-attr`` Set the ``style-src-attr`` directive. A ``tuple`` or ``list``. *default=None* ``style-src-elem`` Set the ``style-src-elem`` directive. A ``tuple`` or ``list``. *default=None* ``base-uri`` Set the ``base-uri`` directive. A ``tuple`` or ``list``. *default=None* Note: This doesn't use ``default-src`` as a fall-back. ``child-src`` Set the ``child-src`` directive. A ``tuple`` or ``list``. *default=None* ``frame-ancestors`` Set the ``frame-ancestors`` directive. A ``tuple`` or ``list``. *default=None* Note: This doesn't use ``default-src`` as a fall-back. ``navigate-to`` Set the ``navigate-to`` directive. A ``tuple`` or ``list``. *default=None* Note: This doesn't use ``default-src`` as a fall-back. ``form-action`` Set the ``form-action`` directive. A ``tuple`` or ``list``. *default=None* Note: This doesn't use ``default-src`` as a fall-back. ``sandbox`` Set the ``sandbox`` directive. A ``tuple`` or ``list``. *default=None* Note: This doesn't use ``default-src`` as a fall-back. ``report-uri`` Set the ``report-uri`` directive. A ``tuple`` or ``list`` of URIs. Each URI can be a full or relative URI. *default=None* Note: This doesn't use ``default-src`` as a fall-back. ``report-to`` Set the ``report-to`` directive. A ``string`` describing a reporting group. *default=None* See Section 1.2: https://w3c.github.io/reporting/#group Also `see this MDN note on `_ ``report-uri`` and ``report-to``. ``manifest-src`` Set the ``manifest-src`` directive. A ``tuple`` or ``list``. *default=None* ``worker-src`` Set the ``worker-src`` directive. A ``tuple`` or ``list``. *default=None* ``require-sri-for`` Set the ``require-sri-for`` directive. A ``tuple`` or ``list``. *default=None* Valid values: a ``list`` containing ``'script'``, ``'style'``, or both. Spec: require-sri-for-known-tokens_ ``upgrade-insecure-requests`` Include ``upgrade-insecure-requests`` directive. A ``boolean``. *default=False* Spec: upgrade-insecure-requests_ ``require-trusted-types-for`` Include ``require-trusted-types-for`` directive. A ``tuple`` or ``list``. *default=None* Valid values: ``["'script'"]`` ``trusted-types`` Include ``trusted-types`` directive. A ``tuple`` or ``list``. *default=None* Valid values: a ``list`` of allowed policy names that may include ``default`` and/or ``'allow-duplicates'`` Deprecated CSP settings ----------------------- The following ``DIRECTIVES`` settings are still configurable, but are considered deprecated in terms of the latest implementation of the relevant spec. ``block-all-mixed-content`` Include ``block-all-mixed-content`` directive. A ``boolean``. *default=False* Related `note on MDN `_. Spec: block-all-mixed-content_ ``plugin-types`` Set the ``plugin-types`` directive. A ``tuple`` or ``list``. *default=None* Note: This doesn't use ``default-src`` as a fall-back. Related `note on MDN `_. ``prefetch-src`` Set the ``prefetch-src`` directive. A ``tuple`` or ``list``. *default=None* Related `note on MDN `_. Changing the Policy ~~~~~~~~~~~~~~~~~~~ The policy can be changed on a per-view (or even per-request) basis. See the :ref:`decorator documentation ` for more details. .. _Content-Security-Policy: https://www.w3.org/TR/CSP/ .. _Content-Security-Policy-L3: https://w3c.github.io/webappsec-csp/ .. _spec: Content-Security-Policy_ .. _require-sri-for-known-tokens: https://w3c.github.io/webappsec-subresource-integrity/#opt-in-require-sri-for .. _upgrade-insecure-requests: https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery .. _block-all-mixed-content: https://w3c.github.io/webappsec-mixed-content/ .. _block-all-mixed-content_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/block-all-mixed-content .. _plugin_types_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/plugin-types .. _prefetch_src_mdn: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/prefetch-src .. _strict-csp: https://csp.withgoogle.com/docs/strict-csp.html mozilla-django-csp-6d5cc6c/docs/contributing.rst000066400000000000000000000055601477325434700221220ustar00rootroot00000000000000.. _contributing-chapter: ============ Contributing ============ Patches are more than welcome! You can find the issue tracker `on GitHub `_ and we'd love pull requests. Setup ===== To install all the requirements (probably into a virtualenv_): .. code-block:: bash pip install -e . pip install -e ".[dev]" This installs: * All the text requirements * All the typing requirements * pre-commit_, for checking styles * tox_, for running tests against multiple environments * Sphinx_ and document building requirements Style ===== Patches should follow PEP8_ and should not introduce any new violations as detected by the ruff_ tool. To help stay on top of this, install pre-commit_, and then run ``pre-commit install-hooks``. Now you'll be set up to auto-format your code according to our style and check for errors for every commit. Tests ===== Patches fixing bugs should include regression tests (ideally tests that fail without the rest of the patch). Patches adding new features should test those features thoroughly. To run the tests, install the requirements (probably into a virtualenv_): .. code-block:: bash pip install -e . pip install -e ".[tests]" Then just `pytest`_ to run the tests: .. code-block:: bash pytest To run the tests with coverage and get a report, use the following command: .. code-block:: bash pytest --cov=csp --cov-config=.coveragerc To run the tests like Github Actions does, you'll need pyenv_: .. code-block:: bash pyenv install 3.9 3.10 3.11 3.12 pypy3.9 pypy3.10 pyenv local 3.9 3.10. 3.11 3.12 pypy3.9 pypy3.10 pip install -e ".[dev]" # installs tox tox # run sequentially tox run-parallel # run in parallel, may cause issues on coverage step tox -e 3.12-4.2.x # run tests on Python 3.12 and Django 4.x tox --listenvs # list all the environments Type Checking ============= New code should have type annotations and pass mypy_ in strict mode. Use the typing syntax available in the earliest supported Python version 3.9. To check types: .. code-block:: bash pip install -e ".[typing]" mypy . If you make a lot of changes, it can help to clear the mypy cache: .. code-block:: bash mypy --no-incremental . Updating Documentation ====================== To rebuild documentation locally: .. code-block:: bash pip install -e ".[dev]" cd docs make html open _build/html/index.html # On macOS .. _PEP8: http://www.python.org/dev/peps/pep-0008/ .. _Sphinx: https://www.sphinx-doc.org/en/master/index.html .. _mypy: https://mypy.readthedocs.io/en/stable/ .. _pre-commit: https://pre-commit.com/#install .. _pyenv: https://github.com/pyenv/pyenv .. _pytest: https://pytest.org/latest/usage.html .. _ruff: https://pypi.org/project/ruff/ .. _tox: https://tox.wiki/en/stable/ .. _virtualenv: http://www.virtualenv.org/ mozilla-django-csp-6d5cc6c/docs/decorators.rst000066400000000000000000000110451477325434700215530ustar00rootroot00000000000000.. _decorator-chapter: ==================================== Modifying the Policy with Decorators ==================================== Content Security Policies should be restricted and paranoid by default. You may, on some views, need to expand or change the policy. django-csp includes four decorators to help. All decorators take an optional keyword argument, ``REPORT_ONLY``, which defaults to ``False``. If set to ``True``, the decorator will update the report-only policy instead of the enforced policy. ``@csp_exempt`` =============== Using the ``@csp_exempt`` decorator disables the CSP header on a given view. .. code-block:: python from csp.decorators import csp_exempt # Will not have a CSP header. @csp_exempt() def myview(request): return render(...) # Will not have a CSP report-only header. @csp_exempt(REPORT_ONLY=True) def myview(request): return render(...) You can manually set this on a per-response basis by setting the ``_csp_exempt`` or ``_csp_exempt_ro`` attribute on the response to ``True``: .. code-block:: python # Also will not have a CSP header. def myview(request): response = render(...) response._csp_exempt = True return response ``@csp_update`` =============== The ``@csp_update`` header allows you to **append** values to the source lists specified in the settings. If there is no setting, the value passed to the decorator will be used verbatim. .. note:: To quote the CSP spec: "There's no inheritance; ... the default list is not used for that resource type" if it is set. E.g., the following will not allow images from 'self': default-src 'self'; img-src imgsrv.com The arguments to the decorator are the same as the :ref:`settings `. The decorator expects a single dictionary argument, where the keys are the directives and the values are either strings, lists or tuples. An optional argument, ``REPORT_ONLY``, can be set to ``True`` to update the report-only policy instead of the enforced policy. .. code-block:: python from csp.decorators import csp_update # Will append imgsrv.com to the list of values for `img-src` in the enforced policy. @csp_update({"img-src": "imgsrv.com"}) def myview(request): return render(...) # Will append cdn-img.com to the list of values for `img-src` in the report-only policy. @csp_update({"img-src": "cdn-img.com"}, REPORT_ONLY=True) def myview(request): return render(...) ``@csp_replace`` ================ The ``@csp_replace`` decorator allows you to **replace** a source list specified in settings. If there is no setting, the value passed to the decorator will be used verbatim. (See the note under ``@csp_update``.) If the specified value is None, the corresponding key will not be included. The arguments and values are the same as ``@csp_update``: .. code-block:: python from csp.decorators import csp_replace # Will allow images only from imgsrv2.com in the enforced policy. @csp_replace({"img-src": "imgsrv2.com"}) def myview(request): return render(...) # Will allow images only from cdn-img2.com in the report-only policy. @csp_replace({"img-src": "cdn-img2.com"}, REPORT_ONLY=True) def myview(request): return render(...) The ``csp_replace`` decorator can also be used to remove a directive from the policy by setting the value to ``None``. For example, if the ``frame-ancestors`` directive is set in the Django settings and you want to remove the ``frame-ancestors`` directive from the policy for this view: .. code-block:: python from csp.decorators import csp_replace @csp_replace({"frame-ancestors": None}) def myview(request): return render(...) ``@csp`` ======== If you need to set the entire policy on a view, ignoring all the settings, you can use the ``@csp`` decorator. This can be stacked to update both the enforced policy and the report-only policy if both are in use, as shown below. .. code-block:: python from csp.constants import SELF, UNSAFE_INLINE from csp.decorators import csp @csp( { "default-src": [SELF], "img-src": ["imgsrv.com"], "script-src": ["scriptsrv.com", "googleanalytics.com", UNSAFE_INLINE], } ) @csp( { "default-src": [SELF], "img-src": ["imgsrv.com"], "script-src": ["scriptsrv.com", "googleanalytics.com"], "frame-src": [SELF], }, REPORT_ONLY=True, ) def myview(request): return render(...) mozilla-django-csp-6d5cc6c/docs/index.rst000066400000000000000000000011301477325434700205070ustar00rootroot00000000000000.. Django-CSP documentation home. It should contain at least the root `toctree` directive. ========== django-csp ========== django-csp adds Content-Security-Policy_ headers to Django applications. :Version: |release| :Code: https://github.com/mozilla/django-csp :License: BSD; see LICENSE file :Issues: https://github.com/mozilla/django-csp/issues Contents: .. toctree:: :maxdepth: 2 installation configuration migration-guide decorators nonce trusted_types reports contributing .. _Content-Security-Policy: http://www.w3.org/TR/CSP/ mozilla-django-csp-6d5cc6c/docs/installation.rst000066400000000000000000000016161477325434700221120ustar00rootroot00000000000000.. _installation-chapter: ===================== Installing django-csp ===================== First, install django-csp via pip or from source: .. code-block:: bash pip install django-csp Add the csp app to your ``INSTALLED_APPS`` in your project's ``settings`` module: .. code-block:: python INSTALLED_APPS = ( # ... "csp", # ... ) Now edit your project's ``settings`` module, to add the django-csp middleware to ``MIDDLEWARE``, like so: .. code-block:: python MIDDLEWARE = ( # ... "csp.middleware.CSPMiddleware", # ... ) .. Note:: Middleware order does not matter unless you have other middleware modifying the CSP header, or requires CSP features like a nonce. See :ref:`Using the generated CSP nonce` for further advice on middleware order. That should do it! Go on to :ref:`configuring CSP `. mozilla-django-csp-6d5cc6c/docs/migration-guide.rst000066400000000000000000000216741477325434700225030ustar00rootroot00000000000000.. _migration-guide-chapter: ============================== django-csp 4.0 Migration Guide ============================== Overview ======== In the latest version of `django-csp`, the format for configuring Content Security Policy (CSP) settings has been updated are are backwards-incompatible with prior versions. The previous approach of using individual settings prefixed with ``CSP_`` for each directive is no longer supported. Instead, all CSP settings are now consolidated into one of two dict-based settings: ``CONTENT_SECURITY_POLICY`` or ``CONTENT_SECURITY_POLICY_REPORT_ONLY``. Migrating from the Old Settings Format ====================================== Update `django-csp` ------------------- First, update the `django-csp` package to the latest version that supports the new settings format. You can do this by running: .. code-block:: bash pip install -U django-csp Add the `csp` app to `INSTALLED_APPS` ------------------------------------- In your Django project's `settings.py` file, add the `csp` app to the ``INSTALLED_APPS`` setting: .. code-block:: python INSTALLED_APPS = [ # ... "csp", # ... ] Run the Django check command ---------------------------- This is optional but can help kick start the new settings configuration for you. Run the Django check command which will look for old settings and output a configuration in the new format: .. code-block:: bash python manage.py check This can help you identify the existing CSP settings in your project and provide a starting point for migrating to the new format. Identify Existing CSP Settings ------------------------------ Locate all the existing CSP settings in your Django project. These settings start with the ``CSP_`` prefix, such as ``CSP_DEFAULT_SRC``, ``CSP_SCRIPT_SRC``, ``CSP_IMG_SRC``, etc. Create the New Settings Dictionary ---------------------------------- In your Django project's `settings.py` file, create a new dictionary called ``CONTENT_SECURITY_POLICY`` or ``CONTENT_SECURITY_POLICY_REPORT_ONLY``, depending on whether you want to enforce the policy or only report violations, or both. Use the output from the Django check command as a starting point to populate this dictionary. Migrate Existing Settings ------------------------- Migrate your existing CSP settings to the new format by populating the ``DIRECTIVES`` dictionary inside the ``CONTENT_SECURITY_POLICY`` setting. The keys of the ``DIRECTIVES`` dictionary should be the CSP directive names in lowercase, and the values should be lists containing the corresponding sources. The Django check command output can help you identify the directive names and sources. For example, if you had the following old settings: .. code-block:: python CSP_DEFAULT_SRC = ["'self'", "*.example.com"] CSP_SCRIPT_SRC = ["'self'", "js.cdn.com/example/"] CSP_IMG_SRC = ["'self'", "data:", "example.com"] CSP_EXCLUDE_URL_PREFIXES = ["/admin"] The new settings would be: .. code-block:: python from csp.constants import SELF CONTENT_SECURITY_POLICY = { "EXCLUDE_URL_PREFIXES": ["/admin"], "DIRECTIVES": { "default-src": [SELF, "*.example.com"], "script-src": [SELF, "js.cdn.com/example/"], "img-src": [SELF, "data:", "example.com"], }, } .. note:: The keys in the ``DIRECTIVES`` dictionary, the directive names, are in lowercase and use dashes instead of underscores to match the CSP specification. .. note:: If you were using the ``CSP_INCLUDE_NONCE_IN`` setting, this has been removed in the new settings format. **Previously:** You could use the ``CSP_INCLUDE_NONCE_IN`` setting to specify which directives in your Content Security Policy (CSP) should include a nonce. **Now:** You can include a nonce in any directive by adding the ``NONCE`` constant from the ``csp.constants`` module to the list of sources for that directive. For example, if you had ``CSP_INCLUDE_NONCE_IN = ["script-src"]``, this should be updated to include the `NONCE` sentinel in the `script-src` directive values: .. code-block:: python from csp.constants import NONCE, SELF CONTENT_SECURITY_POLICY = { "DIRECTIVES": { "script-src": [SELF, NONCE], # ... }, } .. note:: If you were using the ``CSP_REPORT_PERCENTAGE`` setting, this should be updated to be a float percentage between 0.0 and 100.0. For example, if you had ``CSP_REPORT_PERCENTAGE = 0.1``, this should be updated to ``10.0`` to represent 10% of CSP errors will be reported: .. code-block:: python CONTENT_SECURITY_POLICY = { "REPORT_PERCENTAGE": 10.0, "DIRECTIVES": { "report-uri": "/csp-report/", # ... }, } Remove Old Settings ------------------- After migrating to the new settings format, remove all the old ``CSP_`` prefixed settings from your `settings.py` file. Update the CSP decorators ------------------------- If you are using the CSP decorators in your views, those will need to be updated as well. The decorators now accept a dictionary containing the CSP directives as an argument. For example: .. code-block:: python from csp.decorators import csp_update @csp_update({"default-src": ["another-url.com"]}) def my_view(request): ... Additionally, each decorator now takes an optional ``REPORT_ONLY`` argument to specify whether the policy should be enforced or only report violations. For example: .. code-block:: python from csp.constants import SELF from csp.decorators import csp @csp({"default-src": [SELF]}, REPORT_ONLY=True) def my_view(request): ... Due to the addition of the ``REPORT_ONLY`` argument and for consistency, the ``csp_exempt`` decorator now requires parentheses when used with and without arguments. For example: .. code-block:: python from csp.decorators import csp_exempt @csp_exempt() @csp_exempt(REPORT_ONLY=True) def my_view(request): ... Look for uses of the following decorators in your code: ``@csp``, ``@csp_update``, ``@csp_replace``, and ``@csp_exempt``. Migrating Custom Middleware =========================== The `CSPMiddleware` has changed in order to support easier extension via subclassing. The `CSPMiddleware.build_policy` and `CSPMiddleware.build_policy_ro` methods have been deprecated in 4.0 and replaced with a new method `CSPMiddleware.build_policy_parts`. .. note:: The deprecated methods will be removed in 4.1. Unlike the old methods, which returned the built CSP policy header string, `build_policy_parts` returns a dataclass that can be modified and updated before the policy is built. This allows custom middleware to modify the policy whilst inheriting behaviour from the base classes. An existing custom middleware, such as this: .. code-block:: python from django.http import HttpRequest, HttpResponseBase from csp.middleware import CSPMiddleware, PolicyParts class ACustomMiddleware(CSPMiddleware): def build_policy(self, request: HttpRequest, response: HttpResponseBase) -> str: config = getattr(response, "_csp_config", None) update = getattr(response, "_csp_update", None) replace = getattr(response, "_csp_replace", {}) nonce = getattr(request, "_csp_nonce", None) # ... do custom CSP policy logic ... return build_policy(config=config, update=update, replace=replace, nonce=nonce) def build_policy_ro(self, request: HttpRequest, response: HttpResponseBase) -> str: config = getattr(response, "_csp_config_ro", None) update = getattr(response, "_csp_update_ro", None) replace = getattr(response, "_csp_replace_ro", {}) nonce = getattr(request, "_csp_nonce", None) # ... do custom CSP report-only policy logic ... return build_policy(config=config, update=update, replace=replace, nonce=nonce) can be replaced with this: .. code-block:: python from django.http import HttpRequest, HttpResponseBase from csp.middleware import CSPMiddleware, PolicyParts class ACustomMiddleware(CSPMiddleware): def get_policy_parts( self, request: HttpRequest, response: HttpResponseBase, report_only: bool = False, ) -> PolicyParts: policy_parts = super().get_policy_parts(request, response, report_only) if report_only: ... # do custom CSP report-only policy logic else: ... # do custom CSP policy logic return policy_parts Conclusion ========== By following this migration guide, you should be able to successfully update your Django project to use the new dict-based CSP settings format introduced in the latest version of `django-csp`. This change aligns the package with the latest CSP specification and provides a more organized and flexible way to configure your Content Security Policy. mozilla-django-csp-6d5cc6c/docs/nonce.rst000066400000000000000000000133461477325434700205160ustar00rootroot00000000000000============================== Using the generated CSP nonce ============================== When ``NONCE`` is included in a directive, the nonce value is returned in the CSP headers **if it is used**, e.g. by evaluating the nonce in your template. To actually make the browser do anything with this value, you will need to include it in the attributes of the tags that you wish to mark as safe. .. Note:: Use view source on a page to see nonce values. **Nonce values are not visible in browser developer tools.** To prevent malicious CSS selectors leaking the values, `they are not exposed to the DOM `_. ``Middleware`` ============== Installing the middleware creates a lazily evaluated property ``csp_nonce`` and attaches it to all incoming requests. .. code-block:: python MIDDLEWARE = ( # ... "csp.middleware.CSPMiddleware", # ... ) This value can be accessed directly on the request object in any view or template and manually appended to any script element like so - .. code-block:: html Assuming the ``NONCE`` sentinel is included in the ``script-src`` directive, this will result in the above script being allowed. .. Note:: The nonce will only be included in the CSP header if: - ``csp.constants.NONCE`` is present in the ``script-src`` or ``style-src`` directives, **and** - ``request.csp_nonce`` is accessed during the request lifecycle, after the middleware processes the request but before it processes the response. The ``csp.middleware.CSPMiddleware`` will include the nonce in the CSP header as it processes the response, but only if it was generated by code reading ``str(request.csp_nonce)``. If code reads an un-generated ``request.csp_nonce`` after the middleware processes the response, it is probably a programming error. In this case, attempting to read the nonce (like ``str(request.csp_nonce)``) will raise a ``csp.exceptions.CSPNonceError``. If the nonce was generated and included in the CSP header, then reading ``request.csp_nonce`` is safe. It is always safe to test ``request.csp_nonce``, such as ``bool(request.csp_nonce)`` or in a conditional like ``if request.csp_nonce: ...``. This will return ``True`` if the nonce was accessed and generated, and ``False`` if not acccesed or generated yet. If other middleware or a later process needs to access ``request.csp_nonce``, then there are a few options: * The middleware can be placed after ``csp.middleware.CSPMiddleware`` in the ``MIDDLEWARE`` setting. This ensures that the middleware generates the nonce before ``CSPMiddleware`` writes the CSP header. * Add a later middleware that accesses the nonce. For example, this function: .. code-block:: python def init_csp_nonce_middleware(get_response): def middleware(request): str(getattr(request, "csp_nonce", None)) return get_response(request) return middleware could be added to the ``MIDDLEWARE`` list: .. code-block:: python MIDDLEWARE = ( "my.middleware.ThatUsesCSPNonce", # ... "csp.middleware.CSPMiddleware", # ... "my.middleware.init_csp_nonce_middleware", ) ``Context Processor`` ===================== This library contains an optional context processor, adding ``csp.context_processors.nonce`` to your configured context processors exposes a variable called ``CSP_NONCE`` into the global template context. This is simple shorthand for ``request.csp_nonce``, but can be useful if you have many occurrences of script tags. .. code-block:: jinja ``Django Template Tag/Jinja Extension`` ======================================= .. note:: If you're making use of ``csp.extensions.NoncedScript`` you need to have ``jinja2>=2.9.6`` installed, so please make sure to either use ``django-csp[jinja2]`` in your requirements or define it yourself. It can be easy to forget to include the ``nonce`` property in a script tag, so there is also a ``script`` template tag available for both Django templates and Jinja environments. This tag will output a properly nonced script every time. For the sake of syntax highlighting, you can wrap the content inside of the ``script`` tag in `` {% endscript %} Jinja ----- Add ``csp.extensions.NoncedScript`` to the TEMPLATES section of your settings file: .. code-block:: python TEMPLATES = [ { "BACKEND": "django.template.backends.jinja2.Jinja2", "OPTIONS": { "extensions": [ "csp.extensions.NoncedScript", ], }, } ] .. code-block:: jinja {% script type="application/javascript" async=False %} {% endscript %} Both templates output the following with a different nonce: .. code-block:: html mozilla-django-csp-6d5cc6c/docs/reports.rst000066400000000000000000000022231477325434700211020ustar00rootroot00000000000000.. _reports-chapter: ===================== CSP Violation Reports ===================== When something on a page violates the Content-Security-Policy, and the policy defines a ``report-uri`` directive, the user agent may POST a report_. Reports are JSON blobs containing information about how the policy was violated. Note: django-csp no longer handles report processing itself, so you will need to stand up your own app to receive them, or else make use of a third-party report processing service. Throttling the number of reports -------------------------------- To throttle the number of requests made to your ``report-uri`` endpoint, you can use ``csp.contrib.rate_limiting.RateLimitedCSPMiddleware`` instead of ``csp.middleware.CSPMiddleware`` and set the ``REPORT_PERCENTAGE`` option: ``REPORT_PERCENTAGE`` Percentage of requests that should see the ``report-uri`` directive. Use this to throttle the number of CSP violation reports made to your ``report-uri``. A **float** between 0.0 and 100.0 (0.0 = no reports at all, 100.0 = always report). Ignored if ``report-uri`` isn't set. .. _report: http://www.w3.org/TR/CSP/#sample-violation-report mozilla-django-csp-6d5cc6c/docs/requirements.txt000066400000000000000000000000211477325434700221300ustar00rootroot00000000000000sphinx_rtd_theme mozilla-django-csp-6d5cc6c/docs/trusted_types.rst000066400000000000000000000145141477325434700223300ustar00rootroot00000000000000=================================== Implementing Trusted Types with CSP =================================== ``DOM Cross-site Scripting`` ============================ Cross-site scripting (XSS) is one of the most prevalent vulnerabilities on the web. Nonce-based CSP is used to prevent server-side XSS. Trusted Types are used to prevent client-side or DOM-XSS_. Trusted Types rely on the browser to enforce the policy that is provided to it. Currently, Trusted Types are supported on Chrome 83 and Android Webview. Many browsers are in the process of adding support. Check back for updated compatibility_. Follow the simple steps below to make your web application Trusted Types compliant. ``Step 1: Enable Trusted Types and Report Only Mode`` ===================================================== Trusted Types require data to be processed before being sent to a risky "sink" where DOM XSS might occur, such as when assigning to Element.innerHTML or calling document.write. When enforced, Trusted Types will tell the browser to block any data that is not properly processed. In order to avoid this, you must fix offending parts of your code. To see where adjustments will be required, turn on trusted types and report only mode. Configure django-csp so that the directive ``require-trusted-types-for`` is set to *‘script’*. Configure the django-csp report-only policy. Configure django-csp so that the directive ``report-uri`` is set to an app or CSP report processing service that you control. Now trusted types violations will be reported to your ``report-uri`` without blocking any of your application’s functionalities. ``Step 2: Fixing Trusted Types Violations`` =========================================== There are four ways to resolve trusted types violations. They are explained here in order of preference. Rewrite the Code ---------------- It may be possible for your code to be rewritten without using dangerous functions. For example, instead of dynamically placing an image using the dangerous ``innerHTML`` sink, the image could be created with ``document.createElement`` and placed using the ``appendChild`` function. Rewriting may be possible for any of the dangerous sinks, which are listed here. * Script manipulation: * ``