Repository: TypeError/secure.py Branch: main Commit: 0d90d71d6843 Files: 80 Total size: 340.1 KB Directory structure: gitextract___lgw2bt/ ├── .github/ │ └── workflows/ │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs/ │ ├── README.md │ ├── configuration.md │ ├── frameworks.md │ ├── headers/ │ │ ├── cache_control.md │ │ ├── content_security_policy.md │ │ ├── cross-origin-resource-policy.md │ │ ├── cross_origin_embedder_policy.md │ │ ├── cross_origin_opener_policy.md │ │ ├── custom_header.md │ │ ├── dns_prefetch_control.md │ │ ├── permissions_policy.md │ │ ├── referrer_policy.md │ │ ├── server.md │ │ ├── strict_transport_security.md │ │ ├── x-permitted-cross-domain-policies.md │ │ ├── x_content_type_options.md │ │ └── x_frame_options.md │ ├── installation.md │ ├── migration.md │ ├── security_considerations.md │ └── usage.md ├── pyproject.toml ├── secure/ │ ├── __init__.py │ ├── _internal/ │ │ ├── __init__.py │ │ ├── configured_headers.py │ │ ├── constants.py │ │ ├── emit.py │ │ ├── normalize.py │ │ ├── policy.py │ │ ├── presets.py │ │ └── types.py │ ├── headers/ │ │ ├── __init__.py │ │ ├── _validation.py │ │ ├── base_header.py │ │ ├── cache_control.py │ │ ├── content_security_policy.py │ │ ├── cross_origin_embedder_policy.py │ │ ├── cross_origin_opener_policy.py │ │ ├── cross_origin_resource_policy.py │ │ ├── custom_header.py │ │ ├── permissions_policy.py │ │ ├── referrer_policy.py │ │ ├── server.py │ │ ├── strict_transport_security.py │ │ ├── x_content_type_options.py │ │ ├── x_dns_prefetch_control.py │ │ ├── x_frame_options.py │ │ └── x_permitted_cross_domain_policies.py │ ├── middleware/ │ │ ├── __init__.py │ │ ├── asgi.py │ │ └── wsgi.py │ ├── py.typed │ └── secure.py └── tests/ ├── __init__.py ├── headers/ │ ├── test_cache_control.py │ ├── test_content_security_policy.py │ ├── test_cross_origin_embedder_policy.py │ ├── test_cross_origin_opener_policy.py │ ├── test_custom_header.py │ ├── test_header_contracts_extended.py │ ├── test_permissions_policy.py │ ├── test_referrer_policy.py │ ├── test_server.py │ ├── test_strict_transport_security.py │ ├── test_x_content_type_options.py │ └── test_x_frame_options.py ├── middleware/ │ ├── test_middleware.py │ └── test_secure_protocols.py └── secure_tests/ ├── __init__.py ├── test_exports.py ├── test_headers.py ├── test_internal_helpers.py └── test_secure.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/tests.yml ================================================ name: Unit Tests on: pull_request: permissions: contents: read jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python: - "3.10" - "3.11" - "3.12" - "3.13" - "3.14-dev" steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Install toolchain run: pip install ruff==0.13.2 pytest - name: Install package run: pip install -e . - name: Unit tests run: python -m pytest - name: Lint run: ruff check secure tests ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ .idea .DS_Store ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - Placeholder for upcoming changes. ## [2.0.1] - 2026-04-21 This is the first stable v2 release. Version `2.0.0` was burned and should be skipped when tagging or publishing. v2 focuses on a cleaner public API, a redesigned preset model, first-class ASGI/WSGI middleware, and stricter, safer header handling. ### Breaking Changes - `Secure.headers` is now strict and read-only - v1: `headers` was a cached `dict[str, str]` and silently collapsed duplicate names - v2: duplicate header names (case-insensitive) raise `ValueError` - use `header_items()` for multi-valued output or `deduplicate_headers()` to resolve duplicates - Default headers have changed - `Secure.with_default_headers()` now maps to `Preset.BALANCED` - v1 default included `Cache-Control: no-store`; v2 does not - applications relying on v1 defaults should explicitly configure required headers - Presets redesigned - new `Preset.BALANCED` (recommended default) - `Preset.BASIC` updated for Helmet.js parity and no longer matches v1 BASIC - `Preset.STRICT` no longer enables HSTS preload by default - cache behavior and header composition differ across presets compared to v1 - FastAPI / ASGI integration model changed in practice - v1 relied on per-response mutation (`set_headers` / `set_headers_async`) - v2 introduces middleware-based integration as the recommended approach ### Added - Middleware - `SecureASGIMiddleware` - `SecureWSGIMiddleware` - `secure.middleware` module - Header pipeline helpers - `allowlist_headers(...)` - `deduplicate_headers(...)` - `validate_and_normalize_headers(...)` - `header_items()` for ordered `(name, value)` output - New header builders and constants - `CrossOriginResourcePolicy` - `XDnsPrefetchControl` - `XPermittedCrossDomainPolicies` - `MULTI_OK`, `COMMA_JOIN_OK`, `DEFAULT_ALLOWED_HEADERS` - policy enums: `OnInvalidPolicy`, `OnUnexpectedPolicy`, `DeduplicateAction` ### Changed - `Secure.with_default_headers()` now returns the balanced preset - Header handling is stricter and fails fast on invalid or duplicate configurations - Header normalization and validation are first-class operations - Response integration is more robust across sync and async frameworks - `headers_list` mutations are now reflected correctly (no stale cached state) - Documentation updated to emphasize middleware usage and preset selection ### Migration Notes - Do not assume v1 defaults - compare emitted headers and explicitly configure any required behavior - Audit any usage of `Secure.headers` - treat as read-only in v2 - use `header_items()` or `deduplicate_headers()` when duplicates are possible - Move to middleware for ASGI/WSGI apps - replace per-response `set_headers_async()` calls with `SecureASGIMiddleware` or `SecureWSGIMiddleware` - Explicitly configure behavior that changed - add `Cache-Control` if you relied on v1 defaults - add HSTS preload manually if required ### Notes - Neither v1 nor v2 exposes `secure.__version__`; use package metadata for version checks ## [1.0.1] - 2024-10-18 ### Fixed - Improved performance of `Secure.set_headers` by reducing redundant type checks. ([#26](https://github.com/TypeError/secure/issues/26)) ## [1.0.0] - 2024-09-27 ### Breaking Changes - Full redesign of the `secure.py` library with modern Python (3.10+) support. - Major API overhaul for improved usability and Pythonic design. ### Added - Enhanced support for FastAPI and asynchronous frameworks. - Added type hints and better type annotations for a smoother developer experience. - Refined default security headers for improved protection across web frameworks. - Support for modern Python features such as the union operator (`|`) and `cached_property`. ## [0.3.0] - 2021-04-27 ### Breaking Changes - Full redesign of Secure API. - Removal of cookie support. ### Added - Added type hints for better developer experience. - Added support for FastAPI. ### Changed - Replaced Feature-Policy with Permissions-Policy (#10). ## [0.2.1] - 2018-12-24 ### Added - Added support for Masonite framework. - Added docstrings for `SecureHeaders` and `SecureCookie`. ### Changed - Upper-cased SameSite enum to `SameSite.LAX` / `SameSite.STRICT`. - Modified hug implementation for SecureHeaders and SecureCookie. - Renamed `Feature.Values.All` to `Feature.Values.All_` to avoid conflict with the built-in `all`. ### Fixed - Removed trailing semicolon from Feature Policy. ## [0.2.0] - 2018-12-16 ### Added - Added policy builder `SecurePolicies` in `policies.py`. - Added `Expires` header for legacy browser support. - Added `max-age` directive to `Cache-Control` header. ### Changed - Renamed `XXS` argument to `XXP`. - Modified `set-cookie` to use Flask's native method. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of Conduct This Code of Conduct applies to all `secure` community spaces. ## Our Pledge We’re committed to a welcoming, safe, equitable community for everyone. Treat others with respect and assume good faith. ## Expected Behavior - Be kind, constructive, and professional. - Respect different viewpoints and experiences. - Take responsibility for your actions and help repair harm. - Give and accept feedback gracefully. ## Unacceptable Behavior - Harassment, threats, or hate/discrimination. - Personal attacks, sexualized behavior, or stereotyping. - Sharing someone’s private information without consent. - Impersonation, misleading identity, or evasions of enforcement. - Spam/promotional content outside community norms. ## Reporting Report issues to **caleb@typeerror.com**. Maintainers will review reports promptly and handle them as confidentially as possible. ## Enforcement Maintainers may take action appropriate to the situation, including warnings, temporary limits, suspension, or a permanent ban. ## Scope Applies in project spaces (issues, PRs, discussions, chats) and when representing the project publicly. ## Attribution Adapted from the Contributor Covenant v3.0: https://www.contributor-covenant.org/version/3/0/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Thanks for helping make `secure` better. The following guidance keeps contributions aligned with the project’s release-quality standards. ## Development environment 1. Create a virtual environment and activate it: ```bash python -m venv .venv source .venv/bin/activate ``` 2. Install the package in editable mode so local changes are picked up automatically: ```bash pip install -e . ``` 3. Install the tooling used by the project: ```bash pip install pytest ruff ``` _Optional:_ `uv` is the package manager used by the project for releases; you can use `uv run pytest` and `uv add ...` to manage dependencies, but it is not required for local development. ## Running tests, linting, and formatting - **Run unit tests:** `pytest` - **Run the linter:** `ruff check` - **Apply formatting / fix issues:** `ruff format` Run these commands before opening a pull request. If you rely on a different Python version, keep it within the supported range (Python 3.10+). ## Adding a header document 1. Add a new guide under `docs/headers/` named after the header (for example, `docs/headers/example_header.md`). 2. Mirror the structure of the existing header docs: - Start with a **Purpose** section that explains the header’s intent. - Describe the **Default behavior** and mention how the builder models that default. - Show a **Using with `Secure`** example and describe the builder API with method names. - Include **Resources** / **Attribution** and any security caveats. 3. Link the new document from `docs/README.md` (under the Security Headers list) so readers can discover it easily. 4. Ensure code snippets use the public API (`from secure import ...`), reference the appropriate response types, and avoid framework-specific terminology unless a callout is necessary. ## Adding a framework example 1. Update `docs/frameworks.md`: - Add the framework to the table of contents. - Include a short intro describing the framework’s model (WSGI vs ASGI, sync vs async). - Provide at least one working example showing how to wire `Secure` (middleware, hooks, or response-level helpers). - Mention the correct response type (`Response`, `JSONResponse`, etc.) or highlight that you are working with the framework’s default response object. 2. If the framework needs extra instructions (e.g., disabling Uvicorn’s `Server` header), document them in the same section. 3. Keep the tone focused on security headers rather than broader framework guidance. ## Commit conventions - Keep commit messages short (<72 characters) and in the imperative (e.g., `docs: clarify defaults`). - Prefix doc-only changes with `docs:` so reviewers immediately know the scope. - Reference any related issue or PR in the description when applicable. - Run linting/tests before committing to minimize follow-up work. ## Pull request checklist - [ ] I have run `pytest` locally (or a representative suite) and addressed any failures. - [ ] I have run `ruff check` and `ruff format` (when formatting attr). - [ ] Documentation updates describe the new behavior (new header docs, framework guidance, etc.). - [ ] If applicable, I have updated the release notes/CHANGELOG entry for new user-visible behavior. - [ ] My changes follow the project’s security and contribution guidelines (this document). ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018-2024 Caleb Kinney Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # secure [![PyPI Version](https://img.shields.io/pypi/v/secure.svg)](https://pypi.org/project/secure/) [![Python Versions](https://img.shields.io/pypi/pyversions/secure.svg)](https://pypi.org/project/secure/) [![License](https://img.shields.io/pypi/l/secure.svg)](https://github.com/TypeError/secure/blob/main/LICENSE) Define HTTP security headers once. Apply them consistently across Python web apps. `secure` provides a small, dependency-free API for configuring modern security headers across common Python web frameworks through ASGI middleware, WSGI middleware, or framework response hooks. Use it when you want to avoid copy-pasted header strings spread across handlers, hooks, and middleware.

secure hex

Quick links: [Quick start](#quick-start) · [Headers](#what-headers-are-applied-by-default) · [Middleware](#middleware) --- ## Why use `secure` Setting headers manually is fine for one endpoint. It gets harder to review when values are copied across routes, response hooks, reverse-proxy settings, and different framework integrations. `secure` helps you: - Keep one `Secure` policy object instead of scattered header strings - Apply the same policy through ASGI middleware, WSGI middleware, or response hooks - Start with practical presets, then customize headers for your application - Use builders for complex headers such as Content Security Policy and Permissions Policy - Make duplicate handling, header overwrites, and validation explicit The defaults are a reasonable starting point, not a substitute for application-specific review. Content Security Policy in particular should be adjusted for the scripts, styles, assets, and third-party services your app actually uses. --- ## Installation ```bash uv add secure # or pip install secure ``` --- ## Quick start ### FastAPI / ASGI middleware Use middleware when you can attach `secure` once and cover the whole application. ```python from fastapi import FastAPI from secure import Secure from secure.middleware import SecureASGIMiddleware app = FastAPI() secure_headers = Secure.with_default_headers() app.add_middleware(SecureASGIMiddleware, secure=secure_headers) ``` ### Response hooks and handlers Use the same policy object in framework hooks, middleware callbacks, or handlers that expose a response object with headers support. ```python from secure import Secure secure_headers = Secure.with_default_headers() secure_headers.set_headers(response) # or await secure_headers.set_headers_async(response) ``` `Secure.with_default_headers()` maps to `Preset.BALANCED`, a practical default designed to be customized. --- ## What headers are applied by default ```http Cross-Origin-Opener-Policy: same-origin Cross-Origin-Resource-Policy: same-origin Content-Security-Policy: default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests Strict-Transport-Security: max-age=31536000; includeSubDomains Permissions-Policy: geolocation=(), microphone=(), camera=() Referrer-Policy: strict-origin-when-cross-origin X-Content-Type-Options: nosniff X-Frame-Options: SAMEORIGIN ``` This preset reflects modern browser guidance from MDN and OWASP. It reduces cross-origin risk, prevents MIME sniffing, and provides a conservative Content Security Policy you can extend. The default CSP avoids unsafe script execution by default, but CSP is context-sensitive. Interactive apps, dashboards, CDNs, analytics, embedded content, or other third-party integrations may require additional configuration. --- ## Presets ```python from secure import Preset, Secure Secure.from_preset(Preset.BALANCED) Secure.from_preset(Preset.BASIC) Secure.from_preset(Preset.STRICT) ``` - **BALANCED**: practical default for many applications - **BASIC**: Helmet-style compatibility - **STRICT**: tighter CSP and isolation Start with BALANCED, review the generated headers, and move stricter only when needed. --- ## Middleware `secure` provides both ASGI and WSGI middleware. - Overwrites headers by default - Allows controlled duplication via `multi_ok` - Only modifies HTTP responses in ASGI apps ### ASGI ```python from secure import Secure from secure.middleware import SecureASGIMiddleware secure_headers = Secure.with_default_headers() app = SecureASGIMiddleware(app, secure=secure_headers) ``` ### WSGI ```python from secure import Secure from secure.middleware import SecureWSGIMiddleware secure_headers = Secure.with_default_headers() app = SecureWSGIMiddleware(app, secure=secure_headers) ``` ### Django example ```python from secure import Secure class SecureHeadersMiddleware: def __init__(self, get_response): self.get_response = get_response self.secure = Secure.with_default_headers() def __call__(self, request): response = self.get_response(request) self.secure.set_headers(response) return response ``` --- ## Framework integration examples These examples show common integration paths. See the full [framework integration guide](https://github.com/TypeError/secure/tree/main/docs/frameworks.md) for additional frameworks and notes about first-class middleware versus minimal fallback examples. ### FastAPI ```python from secure.middleware import SecureASGIMiddleware app.add_middleware(SecureASGIMiddleware, secure=secure_headers) ``` ### Flask ```python @app.after_request def add_security_headers(response): secure_headers.set_headers(response) return response ``` ### Shiny for Python ```python from shiny import App from secure.middleware import SecureASGIMiddleware app = SecureASGIMiddleware(App(), secure=secure_headers) ``` Interactive apps often need CSP configuration for their script, style, and asset loading patterns. --- ## Policy builders ### Content Security Policy ```python from secure.headers import ContentSecurityPolicy csp = ( ContentSecurityPolicy() .default_src("'self'") .script_src("'self'", "cdn.example.com") ) ``` ### Permissions Policy ```python from secure.headers import PermissionsPolicy permissions = ( PermissionsPolicy() .geolocation("'self'") .camera("'none'") ) ``` --- ## Header pipeline and validation Use the optional pipeline when you need stricter control: ```python secure_headers = ( Secure.with_default_headers() .allowlist_headers(...) .deduplicate_headers(...) .validate_and_normalize_headers(...) ) ``` This makes header allowlists, deduplication, normalization, and validation explicit instead of leaving those choices spread across application code. --- ## Requirements - Python 3.10+ - No external dependencies --- ## Documentation Read the full [documentation](https://github.com/TypeError/secure/tree/main/docs). --- ## Learn more - [MDN HTTP Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) - [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) --- ## License MIT License --- ## Contributing Issues and pull requests are welcome. --- ## Acknowledgements Built using guidance from MDN and OWASP secure headers recommendations. ================================================ FILE: docs/README.md ================================================ # Documentation Use this index when you know what you need. For a first pass, start with the [top-level README](../README.md), then continue with [Usage](./usage.md). The recommended default throughout the docs is `Secure.with_default_headers()`, which maps to `Preset.BALANCED`. ## Start here - [Installation](./installation.md) - [Usage](./usage.md) - [Framework integration](./frameworks.md) - [Migration notes](./migration.md) ## Reference - [Configuration](./configuration.md) - [Security considerations](./security_considerations.md) ## Header builders - [Cache-Control](./headers/cache_control.md) - [Content-Security-Policy](./headers/content_security_policy.md) - [Cross-Origin-Embedder-Policy](./headers/cross_origin_embedder_policy.md) - [Cross-Origin-Opener-Policy](./headers/cross_origin_opener_policy.md) - [Cross-Origin-Resource-Policy](./headers/cross-origin-resource-policy.md) - [Custom Header](./headers/custom_header.md) - [Permissions-Policy](./headers/permissions_policy.md) - [Referrer-Policy](./headers/referrer_policy.md) - [Server](./headers/server.md) - [Strict-Transport-Security](./headers/strict_transport_security.md) - [X-Content-Type-Options](./headers/x_content_type_options.md) - [X-DNS-Prefetch-Control](./headers/dns_prefetch_control.md) - [X-Frame-Options](./headers/x_frame_options.md) - [X-Permitted-Cross-Domain-Policies](./headers/x-permitted-cross-domain-policies.md) ================================================ FILE: docs/configuration.md ================================================ # Configuration Guide This guide covers the parts of `secure` you are most likely to customize after the quick start. Keep `Secure` as the public entry point and pass it the builders you need. ## Default configuration `Secure.with_default_headers()` uses `Preset.BALANCED`, which provides a modern baseline while keeping the header set lean: - **Cross-Origin-Opener-Policy:** `same-origin` - **Cross-Origin-Resource-Policy:** `same-origin` - **Content-Security-Policy:** `default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests` - **Strict-Transport-Security:** `max-age=31536000; includeSubDomains` - **Permissions-Policy:** `geolocation=(), microphone=(), camera=()` - **Referrer-Policy:** `strict-origin-when-cross-origin` - **Server:** empty string - **X-Content-Type-Options:** `nosniff` - **X-Frame-Options:** `SAMEORIGIN` Balanced intentionally skips `Cache-Control` and the compatibility headers (`X-Permitted-Cross-Domain-Policies`, `X-DNS-Prefetch-Control`, `Origin-Agent-Cluster`, `X-Download-Options`, `X-XSS-Protection`). Add them explicitly when your deployment still depends on them. `Preset.BALANCED` is the recommended default. `Preset.BASIC` is the compatibility-oriented option that most closely matches Helmet-style defaults. `Preset.STRICT` is available when you want tighter CSP and stronger isolation. It is not the default. The balanced CSP includes `'unsafe-inline'` in `style-src` for compatibility. It does not allow inline scripts by default. If your app needs a looser CSP, treat that as an app-specific adjustment and test it against real behavior. ## Customizing individual builders All public header builders are re-exported from `secure`, so most applications can stay on the package-level API. ### `X-Frame-Options` If you want to allow framing only from the same origin, use: ```python from secure import Secure, XFrameOptions secure_headers = Secure(xfo=XFrameOptions().sameorigin()) ``` This protects against clickjacking while still allowing same-origin embedding. ### `Strict-Transport-Security` To enforce HTTPS for all subdomains and opt into preload when you are ready, configure HSTS explicitly: ```python from secure import Secure, StrictTransportSecurity secure_headers = Secure( hsts=StrictTransportSecurity().max_age(63072000).include_subdomains().preload() ) ``` This enforces HTTPS for two years, applies the rule to subdomains, and opts into the preload list. ## Adding custom headers Use `CustomHeader` for application-specific response headers that do not have a dedicated builder: ```python from secure import CustomHeader, Secure custom_header = CustomHeader("X-Custom-Header", "CustomValue") secure_headers = Secure(custom=[custom_header]) ``` ## Starting from a preset Every `Secure` instance exposes its configured builders through `headers_list`, so you can replace or extend a preset after construction: ```python from secure import Preset, Secure, StrictTransportSecurity secure_headers = Secure.from_preset(Preset.BALANCED) secure_headers.headers_list = [ header for header in secure_headers.headers_list if header.header_name != "Strict-Transport-Security" ] secure_headers.headers_list.append( StrictTransportSecurity().max_age(63072000).include_subdomains() ) ``` This replaces the preset HSTS builder while leaving the rest of the preset untouched. If you need the compatibility profile instead, start from `Preset.BASIC`. ## Middleware behavior The ASGI and WSGI middleware classes use the same `Secure` instance you would use in hooks or handlers: - Existing header values are overwritten by default for configured header names. - Pass `multi_ok` when a header name should be preserved and appended instead. - `SecureASGIMiddleware` only changes HTTP responses. WebSocket scopes pass through unchanged. ## Validation and normalization If you need stronger guarantees before emission, `Secure` also exposes optional pipeline helpers: - `allowlist_headers(...)` filters or rejects unexpected header names in the current `headers_list`. - `deduplicate_headers(...)` resolves duplicate header names in `headers_list` before you build a single-valued mapping. - `validate_and_normalize_headers(...)` validates and normalizes the current `header_items()`, then caches the single-valued mapping used by `.headers`, `set_headers()`, and `set_headers_async()`. If you intentionally emit duplicate headers such as multiple `Content-Security-Policy` values, use `header_items()` instead of `.headers`. For per-header builder details, see the docs under [headers](./headers). ================================================ FILE: docs/frameworks.md ================================================ # Framework Integration `secure` keeps the same `Secure` object across frameworks. What changes is how you attach it. Some sections below are first-class integrations with clear framework-level hooks or middleware. Others are intentionally minimal fallback examples where support is thinner or framework APIs vary by version. ## How to choose an integration style - Use `set_headers()` when the response object is synchronous and you are already inside a response hook, middleware callback, or view. - Use `set_headers_async()` in async middleware, hooks, or handlers when you want one helper that works safely across async response objects. - Use `SecureWSGIMiddleware` when you want app-wide coverage and can wrap a WSGI application directly. - Use `SecureASGIMiddleware` when you want app-wide coverage in an ASGI stack such as FastAPI, Starlette, or Shiny. Prefer middleware when your framework makes it easy and you want app-wide coverage. Use per-response setters when you are integrating into an existing hook, view, or minimal handler path. ## Middleware behavior `SecureASGIMiddleware` and `SecureWSGIMiddleware` share the same core behavior: - Configured header names are overwritten by default so stale values do not accumulate. - Pass `multi_ok` when a header should be preserved and `secure` should append its own value instead. - The default `secure.MULTI_OK` setting allows controlled duplication for headers such as `Content-Security-Policy`. - `SecureASGIMiddleware` only modifies HTTP responses. WebSocket and other non-HTTP scopes pass through unchanged. Example: ```python from secure import Secure from secure.middleware import SecureASGIMiddleware secure_headers = Secure.with_default_headers() secured_app = SecureASGIMiddleware(app, secure=secure_headers, multi_ok={"content-security-policy"}) ``` ## Uvicorn `Server` header Uvicorn adds `Server: uvicorn` by default. If you want `secure` to control the `Server` header, disable Uvicorn's default header with `--no-server-header` or `server_header=False`. ```python import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000, server_header=False) ``` ## Table of contents - [aiohttp](#aiohttp) - [Bottle](#bottle) - [CherryPy](#cherrypy) - [Dash](#dash) - [Django](#django) - [Falcon](#falcon) - [FastAPI](#fastapi) - [Flask](#flask) - [Masonite](#masonite) - [Morepath](#morepath) - [Pyramid](#pyramid) - [Quart](#quart) - [Responder](#responder) - [Sanic](#sanic) - [Shiny](#shiny) - [Starlette](#starlette) - [Tornado](#tornado) - [TurboGears](#turbogears) - [Custom frameworks](#custom-frameworks) ## aiohttp Async framework with first-class middleware support. ### Recommended: middleware with `set_headers_async()` ```python from aiohttp import web from secure import Secure secure_headers = Secure.with_default_headers() @web.middleware async def add_security_headers(request, handler): response = await handler(request) await secure_headers.set_headers_async(response) return response app = web.Application(middlewares=[add_security_headers]) ``` ### Alternative: set headers in a single handler ```python from aiohttp import web from secure import Secure secure_headers = Secure.with_default_headers() async def home(request): response = web.Response(text="Hello, world") await secure_headers.set_headers_async(response) return response ``` ## Bottle Small WSGI framework with request hooks. ### Recommended: `after_request` hook with `set_headers()` ```python from bottle import Bottle, response from secure import Secure app = Bottle() secure_headers = Secure.with_default_headers() @app.hook("after_request") def add_security_headers(): secure_headers.set_headers(response) ``` ### Fallback: set headers in a route ```python from bottle import Bottle, response from secure import Secure app = Bottle() secure_headers = Secure.with_default_headers() @app.route("/") def home(): secure_headers.set_headers(response) return "Hello, world" ``` ## CherryPy Minimal fallback example. CherryPy exposes the response object in the handler, so handler-level mutation is the practical integration point. ### Minimal fallback: set headers in the exposed method ```python import cherrypy from secure import Secure secure_headers = Secure.with_default_headers() class App: @cherrypy.expose def index(self): secure_headers.set_headers(cherrypy.response) return b"Hello, world" cherrypy.quickstart(App()) ``` ## Dash Dash runs on top of Flask, so the usual Flask integration patterns apply. ### Recommended: Flask `after_request` on `app.server` ```python import dash from dash import html from secure import Secure app = dash.Dash(__name__) server = app.server secure_headers = Secure.with_default_headers() app.layout = html.Div("Hello Dash!") @server.after_request def add_security_headers(response): secure_headers.set_headers(response) return response ``` ### Alternative: `SecureWSGIMiddleware` ```python import dash from dash import html from secure import Secure from secure.middleware import SecureWSGIMiddleware app = dash.Dash(__name__) server = app.server secure_headers = Secure.with_default_headers() app.layout = html.Div("Hello Dash!") server.wsgi_app = SecureWSGIMiddleware(server.wsgi_app, secure=secure_headers) ``` ## Django Django is usually best integrated through Django middleware rather than raw WSGI wrapping. ### Recommended: Django middleware class Register the middleware class in your Django `MIDDLEWARE` setting. ```python from secure import Secure class SecureHeadersMiddleware: def __init__(self, get_response): self.get_response = get_response self.secure = Secure.with_default_headers() def __call__(self, request): response = self.get_response(request) self.secure.set_headers(response) return response ``` ### Fallback: set headers in a view ```python from django.http import HttpResponse from secure import Secure secure_headers = Secure.with_default_headers() def home(request): response = HttpResponse("Hello, world") secure_headers.set_headers(response) return response ``` ## Falcon Falcon exposes a clean response middleware hook. ### Recommended: Falcon middleware ```python import falcon from secure import Secure secure_headers = Secure.with_default_headers() class SecureMiddleware: def process_response(self, req, resp, resource, req_succeeded): secure_headers.set_headers(resp) app = falcon.App(middleware=[SecureMiddleware()]) ``` ### Fallback: set headers in the resource ```python import falcon from secure import Secure secure_headers = Secure.with_default_headers() class HelloWorldResource: def on_get(self, req, resp): resp.text = "Hello, world" secure_headers.set_headers(resp) app = falcon.App() app.add_route("/", HelloWorldResource()) ``` ## FastAPI ASGI framework. Middleware is the clearest default. ### Recommended: `SecureASGIMiddleware` ```python from fastapi import FastAPI from secure import Secure from secure.middleware import SecureASGIMiddleware app = FastAPI() secure_headers = Secure.with_default_headers() app.add_middleware(SecureASGIMiddleware, secure=secure_headers) ``` ### Alternative: `@app.middleware("http")` ```python from fastapi import FastAPI from secure import Secure app = FastAPI() secure_headers = Secure.with_default_headers() @app.middleware("http") async def add_security_headers(request, call_next): response = await call_next(request) await secure_headers.set_headers_async(response) return response ``` ### Fallback: set headers in one route ```python from fastapi import FastAPI, Response from secure import Secure app = FastAPI() secure_headers = Secure.with_default_headers() @app.get("/") def home(response: Response): secure_headers.set_headers(response) return {"hello": "world"} ``` ## Flask WSGI framework with a straightforward response hook. ### Recommended: `after_request` ```python from flask import Flask from secure import Secure app = Flask(__name__) secure_headers = Secure.with_default_headers() @app.after_request def add_security_headers(response): secure_headers.set_headers(response) return response ``` ### Alternative: `SecureWSGIMiddleware` ```python from flask import Flask from secure import Secure from secure.middleware import SecureWSGIMiddleware app = Flask(__name__) secure_headers = Secure.with_default_headers() app.wsgi_app = SecureWSGIMiddleware(app.wsgi_app, secure=secure_headers) ``` ## Masonite Minimal fallback example. Masonite routing and response APIs vary by version, so apply `Secure` to the response object you actually return. ### Minimal fallback: apply to the response you return ```python from secure import Secure secure_headers = Secure.with_default_headers() def home(response): rendered = response.json({"hello": "world"}) secure_headers.set_headers(rendered) return rendered ``` ## Morepath Minimal fallback example. Morepath does not expose a conventional middleware layer for this, so view-level mutation is the practical integration point. ### Minimal fallback: set headers in the view ```python import morepath from secure import Secure secure_headers = Secure.with_default_headers() class App(morepath.App): pass @App.path(path="") class Root: pass @App.view(model=Root) def home(self, request): response = morepath.Response("Hello, world") secure_headers.set_headers(response) return response ``` ## Pyramid Pyramid applications commonly use tweens for cross-cutting response changes. ### Recommended: tween Register the tween in your `Configurator` with `config.add_tween("yourpackage.security.add_security_headers")`. ```python from secure import Secure secure_headers = Secure.with_default_headers() def add_security_headers(handler, registry): def tween(request): response = handler(request) secure_headers.set_headers(response) return response return tween ``` ### Fallback: set headers in a view ```python from pyramid.response import Response from secure import Secure secure_headers = Secure.with_default_headers() def home(request): response = Response("Hello, world") secure_headers.set_headers(response) return response ``` ## Quart Async Flask-compatible framework. ### Recommended: `after_request` with `set_headers_async()` ```python from quart import Quart from secure import Secure app = Quart(__name__) secure_headers = Secure.with_default_headers() @app.after_request async def add_security_headers(response): await secure_headers.set_headers_async(response) return response ``` ### Fallback: set headers in a route ```python from quart import Quart, Response from secure import Secure app = Quart(__name__) secure_headers = Secure.with_default_headers() @app.route("/") async def home(): response = Response("Hello, world") await secure_headers.set_headers_async(response) return response ``` ## Responder Minimal fallback example. Route handlers typically own the response, so route-level mutation is the practical integration point. ### Minimal fallback: set headers in the route ```python import responder from secure import Secure api = responder.API() secure_headers = Secure.with_default_headers() @api.route("/") async def home(req, resp): resp.text = "Hello, world" await secure_headers.set_headers_async(resp) ``` ## Sanic Sanic exposes response middleware for app-wide coverage. ### Recommended: response middleware with `set_headers_async()` ```python from sanic import Sanic from secure import Secure app = Sanic("secure-app") secure_headers = Secure.with_default_headers() @app.middleware("response") async def add_security_headers(request, response): await secure_headers.set_headers_async(response) return response ``` Use route-level setters when you only need a small integration or do not want extra app wiring in tests. ### Fallback: set headers in a route ```python from sanic import Sanic, response from secure import Secure app = Sanic("secure-app") secure_headers = Secure.with_default_headers() @app.get("/") async def home(request): resp = response.text("Hello, world") await secure_headers.set_headers_async(resp) return resp ``` ## Shiny Shiny applications are ASGI apps, so ASGI middleware is the cleanest and most direct path. ### Recommended: `SecureASGIMiddleware` ```python from secure import Secure from secure.middleware import SecureASGIMiddleware from shiny import App, ui secure_headers = Secure.with_default_headers() app_ui = ui.page_fluid("Hello Shiny!") def server(input, output, session): pass app = App(app_ui, server) app = SecureASGIMiddleware(app, secure=secure_headers) ``` If your Shiny app loads external assets or opens additional browser connections, you may need to extend CSP directives such as `connect-src`, `script-src`, or `style-src`. Start from the balanced preset and test the running app before relaxing the policy. ## Starlette ASGI framework. Use ASGI middleware unless you only need route-level control. ### Recommended: `SecureASGIMiddleware` ```python from secure import Secure from secure.middleware import SecureASGIMiddleware from starlette.applications import Starlette from starlette.responses import PlainTextResponse from starlette.routing import Route secure_headers = Secure.with_default_headers() async def home(request): return PlainTextResponse("Hello, world") app = Starlette(routes=[Route("/", home)]) app.add_middleware(SecureASGIMiddleware, secure=secure_headers) ``` ### Alternative: set headers in an endpoint ```python from secure import Secure from starlette.applications import Starlette from starlette.responses import Response from starlette.routing import Route secure_headers = Secure.with_default_headers() async def home(request): response = Response("Hello, world") await secure_headers.set_headers_async(response) return response app = Starlette(routes=[Route("/", home)]) ``` ## Tornado Minimal fallback example. Tornado usually applies headers inside request handlers, so handler-level mutation is the practical integration point. ### Minimal fallback: set headers in the handler ```python import tornado.web from secure import Secure secure_headers = Secure.with_default_headers() class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") secure_headers.set_headers(self) app = tornado.web.Application([(r"/", MainHandler)]) ``` ## TurboGears Minimal fallback example. If you do not already have a framework-level hook in place, controller-level mutation is the practical integration point. ### Minimal fallback: set headers in the controller ```python from tg import Response, TGController, expose from secure import Secure secure_headers = Secure.with_default_headers() class RootController(TGController): @expose() def index(self): response = Response("Hello, world") secure_headers.set_headers(response) return response root = RootController() ``` ## Custom frameworks If your framework is not listed here, the integration rule is still simple: configure one `Secure` instance, then apply it to the response as late as possible before it is sent. ### Recommended: use the response object's setter or headers mapping ```python from secure import Secure secure_headers = Secure.with_default_headers() def add_security_headers(response): secure_headers.set_headers(response) return response ``` ### Fallback: emit header pairs manually ```python from secure import Secure secure_headers = Secure.with_default_headers() for name, value in secure_headers.header_items(): response.headers[name] = value ``` ================================================ FILE: docs/headers/cache_control.md ================================================ # Cache-Control ## What it does `Cache-Control` is a comma-separated list of **directives** that control caching behavior for both **requests** and **responses**. Used correctly, it helps prevent sensitive data from being cached and improves performance for cacheable assets. ## Minimal example ```python from secure import CacheControl, Secure secure_headers = Secure( cache=CacheControl().no_store().max_age(0) ) ``` ## Resulting header ```http Cache-Control: no-store, max-age=0 ``` ## Practical note Use `no-store` for sensitive pages such as sign-in or account settings. `no-cache` is different. It allows storage but requires revalidation before reuse. ## Default behavior If you create `CacheControl()` and do not add directives, it returns the library default value: - **Default header value:** `no-store, max-age=0` This is a secure baseline intended to prevent storage of sensitive responses. ## Using with `Secure` If you do not configure any directives, the default value is emitted. `Preset.STRICT` includes `Cache-Control: no-store, max-age=0`; `Preset.BASIC` and `Preset.BALANCED` leave caching unchanged unless you add this builder. ## Common recipes ### 1) Prevent storing (recommended for sensitive responses) ```python from secure import CacheControl cc = CacheControl() # default: no-store, max-age=0 print(cc.header_name) # Cache-Control print(cc.header_value) # no-store, max-age=0 ``` ### 2) Always revalidate (useful for dynamic HTML) ```python cc = CacheControl().no_cache() print(cc.header_value) # no-cache ``` > Note: `no-cache` does **not** mean “do not store.” It means “store, but revalidate before reuse.” ### 3) Cache-busted static assets (long-lived) If your assets are fingerprinted (e.g., `/app.4f3c1.js`), you can cache them aggressively: ```python cc = CacheControl().public().max_age(31536000).immutable() print(cc.header_value) # public, max-age=31536000, immutable ``` ### 4) Shared caches (CDNs/proxies) vs browser caches ```python cc = CacheControl().s_maxage(604800).max_age(60) print(cc.header_value) # s-maxage=604800, max-age=60 ``` `s-maxage` applies to shared caches and overrides `max-age` for them. ### 5) Stale content during revalidation / on error ```python cc = ( CacheControl() .max_age(604800) .stale_while_revalidate(86400) .stale_if_error(86400) ) print(cc.header_value) # max-age=604800, stale-while-revalidate=86400, stale-if-error=86400 ``` ## Builder API ### Boolean directives (no value) - `.no_store()`, `.no_cache()`, `.no_transform()` - `.public()`, `.private()` - `.must_revalidate()`, `.proxy_revalidate()` - `.immutable()` - `.must_understand()` (recommended to pair with `.no_store()` for safe fallback) ### Parameterized directives (integer seconds) - `.max_age(seconds)` - `.s_maxage(seconds)` - `.min_fresh(seconds)` (request) - `.stale_while_revalidate(seconds)` - `.stale_if_error(seconds)` ### Request directives - `.only_if_cached()` - `.max_stale(seconds=None)` (if omitted, accepts staleness of any age) ## Escape hatches ### `.value("...")` Set an explicit header value (replaces all configured directives): ```python cc = CacheControl().value("no-store, max-age=0") print(cc.header_value) # no-store, max-age=0 ``` ### `.custom("token")` Add a **non-standard / non-MDN** directive token (for niche proxies/CDNs): ```python cc = CacheControl().custom("x-cache-mode=aggressive") print(cc.header_value) # x-cache-mode=aggressive ``` ### `.clear()` Reset to the default (no directives configured; default value will be returned). ## Deterministic output & overwrites - Directives are rendered as a **stable**, comma-separated list. - Repeating a parameterized directive overwrites the previous value (e.g., calling `.max_age(60)` then `.max_age(0)` results in `max-age=0`). - The builder rejects obvious header-splitting primitives (CR/LF) in `.value(...)` and `.custom(...)`. ## Attribution This library implements security recommendations and behavior described by: - [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)) - [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#cache-control) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) ================================================ FILE: docs/headers/content_security_policy.md ================================================ # Content-Security-Policy (CSP) ## What it does The `Content-Security-Policy` (CSP) response header helps mitigate cross-site scripting (XSS), data injection, and related attacks by restricting where content can be loaded from (scripts, styles, images, fonts, connections, frames, etc.). CSP is expressed as a list of **directives** separated by semicolons: ```http Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none' ``` ## Minimal example ```python from secure import ContentSecurityPolicy, Secure csp = ( ContentSecurityPolicy() .default_src("'self'") .object_src("'none'") .base_uri("'self'") ) secure_headers = Secure(csp=csp) ``` ## Resulting header ```http Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self' ``` ## Practical note Do not treat `'unsafe-inline'` as a default starting point for scripts. Use it only as an app-specific compatibility adjustment and test the real app before and after any CSP change. ## Library defaults If you create a `ContentSecurityPolicy()` and do not configure any directives, it returns the library default: ```text default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self' ``` This matches the builder default. The built-in presets use explicit CSP builders rather than this bare builder default. `Preset.BASIC` and `Preset.BALANCED` add `font-src`, `img-src`, `script-src-attr`, `style-src`, and `upgrade-insecure-requests`. ## Best-practice baseline A common “safe baseline” CSP includes: - `default-src 'self'` - `object-src 'none'` - `base-uri 'self'` - `frame-ancestors 'self'` (or `'none'` if you never want to be framed) - `form-action 'self'` - optionally `upgrade-insecure-requests` > CSP is powerful but can break applications if rolled out too aggressively. Start with a report-only policy, review violations, then enforce. --- ## Configuration with `Secure` Once you configure `csp`, apply it in your framework integration: ```python # Flask example from flask import Flask, Response app = Flask(__name__) secure_headers = Secure(csp=csp) @app.after_request def add_security_headers(response: Response) -> Response: secure_headers.set_headers(response) return response ``` ### Report-only mode To observe violations without enforcing (recommended for rollout): ```python csp_report_only = ( ContentSecurityPolicy() .report_only() .default_src("'self'") .script_src("'self'") ) secure_headers = Secure(csp=csp_report_only) ``` Use `.enforce()` to switch back to the enforcing header name. --- ## Fluent directive methods Common methods include: - Fetch directives: `default_src`, `script_src`, `style_src`, `img_src`, `font_src`, `connect_src`, `media_src`, `frame_src`, `worker_src`, `manifest_src`, `fenced_frame_src`, `object_src` - Navigation / embedding: `base_uri`, `form_action`, `frame_ancestors` - Policy controls: `sandbox`, `upgrade_insecure_requests` - Reporting: `report_to`, `report_uri` _(deprecated in MDN)_ ### Deterministic output and deduplication - Each directive name appears at most once. - Tokens passed to a directive are deduplicated (first-seen order preserved). - Serialization is deterministic and uses `"; "` between directives. --- ## Helper utilities ### Keywords Use `keyword()` to safely produce quoted CSP keywords like `'self'` and `'none'`: ```python ContentSecurityPolicy.keyword("self") # "'self'" ContentSecurityPolicy.keyword("none") # "'none'" ``` ### Nonces Use `nonce()` to produce a CSP nonce source expression: ```python nonce_value = "abc123==" # base64 / url-safe base64 ContentSecurityPolicy.nonce(nonce_value) # "'nonce-abc123=='" ``` --- ## Escape hatches ### Set an exact policy string If you need full control (or want to carry over an existing CSP string), use `.value(...)`: ```python csp = ContentSecurityPolicy().value( "default-src 'self'; script-src 'self' https://cdn.example; object-src 'none'" ) ``` `.set(...)` is an alias for `.value(...)`. ### Clear configuration ```python csp = ContentSecurityPolicy().default_src(ContentSecurityPolicy.keyword("self")) csp.clear() # resets back to library default behavior ``` ### Custom directives If you need a directive not covered by a helper method: ```python csp = ( ContentSecurityPolicy() .custom_directive("default-src", "'self'") .custom_directive("script-src", "'self'") ) # `.custom(...)` is an alias ``` --- ## Nonce + `strict-dynamic` example (recommended pattern) When you use nonces, the nonce must be generated **per response** and also placed in your HTML script tag(s). A common pattern is to generate the nonce in request context, then build CSP using it. ### Framework-agnostic CSP construction ```python import secrets from secure import ContentSecurityPolicy nonce = secrets.token_urlsafe(16) csp = ( ContentSecurityPolicy() .default_src("'self'") .script_src( ContentSecurityPolicy.nonce(nonce), ContentSecurityPolicy.keyword("strict-dynamic"), ) .object_src("'none'") ) print(csp.header_value) # default-src 'self'; script-src 'nonce-...' 'strict-dynamic'; object-src 'none' ``` ### Flask pattern (nonce shared via `g`) ```python import secrets from flask import Flask, Response, g from secure import ContentSecurityPolicy, Secure app = Flask(__name__) @app.before_request def set_nonce() -> None: g.csp_nonce = secrets.token_urlsafe(16) @app.after_request def add_security_headers(response: Response) -> Response: csp = ( ContentSecurityPolicy() .default_src("'self'") .script_src( ContentSecurityPolicy.nonce(g.csp_nonce), ContentSecurityPolicy.keyword("strict-dynamic"), ) .style_src("'self'") .object_src("'none'") ) Secure(csp=csp).set_headers(response) return response ``` In your HTML rendering, use the same nonce: ```html ``` --- ## Reporting notes (MDN) - `report-to` is the modern mechanism. - `report-uri` is deprecated in MDN; some browsers that support `report-to` may ignore `report-uri`. - If you need broad compatibility during migration, you may specify both. --- ## References - [MDN: Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy) - [MDN: CSP guide](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) - [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#content-security-policy) ## Attribution This library implements security recommendations and reference material from: - MDN Web Docs (licensed under CC-BY-SA 2.5) - OWASP Secure Headers Project (licensed under CC-BY-SA 4.0) ================================================ FILE: docs/headers/cross-origin-resource-policy.md ================================================ # Cross-Origin-Resource-Policy (CORP) ## What it does The `Cross-Origin-Resource-Policy` (CORP) response header lets a **resource owner** declare what sites/origins are allowed to load that resource. This header is commonly used to reduce cross-origin data leaks by controlling who can load your resources (images, scripts, etc.) and by blocking certain cross-origin/cross-site `no-cors` requests when the policy is more restrictive. ## Minimal example ```python from secure import CrossOriginResourcePolicy, Secure secure_headers = Secure( corp=CrossOriginResourcePolicy().same_origin() ) ``` ## Resulting header ```http Cross-Origin-Resource-Policy: same-origin ``` ## Practical note `same-origin` is a good default for sensitive resources. If you serve shared assets across subdomains, test `same-site` carefully before widening the policy further. ## Best Practices - **`same-origin`**: Strong default for sensitive resources; only allow loads from the same origin. - **`same-site`**: Useful when you need to share resources across subdomains on the same “site” but not with unrelated sites. - **`cross-origin`**: Most permissive; allow any origin to load the resource (use intentionally, not by accident). ## Configuration with `Secure` The `CrossOriginResourcePolicy` class provides a fluent API for setting CORP directives and integrates cleanly with `Secure(...)`. > Library default: if you do not change it, the library’s default value is `same-origin`. > Presets: `Preset.BASIC` and `Preset.BALANCED` include `same-origin`; `Preset.STRICT` does not add CORP by default. ### Methods Available - **`same_origin()`**: Set `Cross-Origin-Resource-Policy: same-origin` - **`same_site()`**: Set `Cross-Origin-Resource-Policy: same-site` - **`cross_origin()`**: Set `Cross-Origin-Resource-Policy: cross-origin` - **`value(value)`**: Set an explicit value (escape hatch; canonicalizes known directives) - **`clear()`**: Reset to the library default value - **`set(value)`**: Backwards-compatible alias for `value(...)` ## Example Usage To restrict resource loading to the same origin: ```python corp = CrossOriginResourcePolicy().same_origin() print(corp.header_name) # Output: 'Cross-Origin-Resource-Policy' print(corp.header_value) # Output: 'same-origin' ``` To allow resource loading from the same site (useful for subdomains): ```python corp = CrossOriginResourcePolicy().same_site() print(corp.header_value) # Output: 'same-site' ``` ## **Attribution** This library implements security recommendations from trusted sources: - [MDN Web Docs: `Cross-Origin-Resource-Policy`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Resource-Policy) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)) - [OWASP Secure Headers Project: Cross-Origin-Resource-Policy](https://owasp.org/www-project-secure-headers/#cross-origin-resource-policy) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) ================================================ FILE: docs/headers/cross_origin_embedder_policy.md ================================================ # Cross-Origin-Embedder-Policy (COEP) ## What it does The **`Cross-Origin-Embedder-Policy`** response header configures the current document’s policy for **loading and embedding cross-origin resources**. At a high level, COEP lets you: - keep the default behavior (`unsafe-none`), - require explicit opt-in via **CORP** (`Cross-Origin-Resource-Policy`) and/or **CORS** (`require-corp`), or - allow some cross-origin loading while **stripping credentials** (`credentialless`). ## Minimal example ```python from secure import CrossOriginEmbedderPolicy, CrossOriginOpenerPolicy, Secure secure_headers = Secure( coep=CrossOriginEmbedderPolicy().require_corp(), coop=CrossOriginOpenerPolicy().same_origin(), ) ``` ## Resulting header ```http Cross-Origin-Embedder-Policy: require-corp ``` ## Practical note COEP is most useful when you are intentionally working toward cross-origin isolation. It can break third-party assets that do not send compatible CORP or CORS headers, so test the full app before enabling it broadly. ## Directive values COEP is a **single-value** header (choose one): | Value | Meaning | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `unsafe-none` | Allows cross-origin resources **without** explicit permission via CORP or CORS. _(This is the browser default if the header is not sent.)_ | | `require-corp` | Blocks cross-origin resource loading unless the resource is permitted via **CORP** (for `no-cors` requests) or via **CORS** (for `cors` requests). | | `credentialless` | Allows `no-cors` cross-origin resource loading **without** CORP opt-in, but sends requests **without credentials** (cookies omitted and ignored). For other request modes, behavior matches `require-corp`. | ## Library default vs browser default - **Browser behavior when the header is absent:** `unsafe-none`. - **This library’s builder default:** `require-corp` (a stricter, security-forward default). If you want “no-op” behavior, you must explicitly choose it: ```python from secure import CrossOriginEmbedderPolicy coep = CrossOriginEmbedderPolicy().unsafe_none() ``` ## Cross-origin isolation (COOP + COEP) Some powerful browser features require your document to be **cross-origin isolated**. To enable this, you generally need: - `Cross-Origin-Embedder-Policy: require-corp` **or** `credentialless`, and - `Cross-Origin-Opener-Policy: same-origin`. ## Usage with `Secure` You can inspect the emitted header pairs with `secure_headers.header_items()` if you need to confirm the final output. `Preset.STRICT` includes COEP by default; `Preset.BASIC` and `Preset.BALANCED` do not. ## Header builder API ```python from secure import CrossOriginEmbedderPolicy coep = ( CrossOriginEmbedderPolicy() .credentialless() # or .require_corp() / .unsafe_none() ) print(coep.header_name) # "Cross-Origin-Embedder-Policy" print(coep.header_value) # "credentialless" ``` ### Methods - `unsafe_none()`: set the value to `unsafe-none` - `require_corp()`: set the value to `require-corp` - `credentialless()`: set the value to `credentialless` - `set(value)`: set a custom value - `clear()`: reset to the library default (`require-corp`) ## Notes / gotchas - `require-corp` can break embedding third-party resources unless they opt-in via CORP or are requested in `cors` mode. - `credentialless` can be a pragmatic alternative for some `no-cors` resources, but it comes with the tradeoff of **no cookies/credentials**. ## Attribution This library implements security recommendations and definitions from trusted sources: - [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy) (CC-BY-SA 2.5) - [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#cross-origin-embedder-policy) (CC-BY-SA 4.0) ================================================ FILE: docs/headers/cross_origin_opener_policy.md ================================================ # Cross-Origin-Opener-Policy ## What it does The `Cross-Origin-Opener-Policy` (COOP) response header controls whether documents opened via `Window.open()` (or navigations) share the same **browsing context group (BCG)** as their opener. When a document is opened into a new BCG, references between the opener and the opened document are severed, which helps mitigate cross-origin attacks often referred to as **XS-Leaks**. ## Minimal example ```python from secure import CrossOriginOpenerPolicy, Secure secure_headers = Secure( coop=CrossOriginOpenerPolicy().same_origin() ) ``` ## Resulting header ```http Cross-Origin-Opener-Policy: same-origin ``` ## Practical note `same-origin` is a strong default, but popup-based flows such as OAuth or payment providers sometimes need `same-origin-allow-popups`. Test those flows before tightening COOP. ## Defaults - **Browser/spec behavior:** If the header is **absent**, the effective behavior is equivalent to `unsafe-none` (opt-out). - **Library default:** This library’s builder defaults to `same-origin` (a secure default), and the built-in presets also configure COOP as `same-origin`. ## Best Practices - **`same-origin`**: Strong isolation; commonly used for cross-origin isolation (often paired with COEP). - **`same-origin-allow-popups`**: Like `same-origin`, but relaxes behavior for integrations that open trusted popups/tabs that opt out (e.g., OAuth/payment flows). - **`noopener-allow-popups`**: Always isolates into a new BCG (except when opened by a same-origin document that also uses `noopener-allow-popups`). Useful when you need to isolate **same-origin** apps from each other (e.g., `/chat` vs `/passwords`) while still allowing popups. - **`unsafe-none`**: Opts out of COOP isolation. ## Configuration with `Secure` Use the `CrossOriginOpenerPolicy` builder and pass it into `Secure(...)`. ## Methods Available Directive helpers (recommended): - `same_origin()` - `same_origin_allow_popups()` - `noopener_allow_popups()` - `unsafe_none()` Escape hatches: - `value("...")` / `custom("...")`: Set a raw value (rejects CR/LF). - `set("...")`: Backwards-compatible alias for `value(...)`. - `clear()`: Reset back to the library default (`same-origin`). ## Example Usage ```python from secure import CrossOriginOpenerPolicy, Secure coop = CrossOriginOpenerPolicy().same_origin() print(coop.header_name) # 'Cross-Origin-Opener-Policy' print(coop.header_value) # 'same-origin' secure_headers = Secure(coop=coop) ``` ## Notes - For **cross-origin isolation** (e.g., `SharedArrayBuffer`), COOP is typically paired with **COEP** (often `require-corp`), and your app must satisfy other isolation requirements. ## Attribution This library implements security recommendations from trusted sources: - [MDN Web Docs: Cross-Origin-Opener-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Opener-Policy) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)) - [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#cross-origin-opener-policy) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) ================================================ FILE: docs/headers/custom_header.md ================================================ # CustomHeader Class ## What it does The `CustomHeader` class lets you create arbitrary HTTP response headers when `secure` does not provide a dedicated builder. ## Minimal example ```python from secure import CustomHeader, Secure custom_header = CustomHeader("X-Custom-Header", "CustomValue") secure_headers = Secure(custom=[custom_header]) ``` ## Resulting header ```http X-Custom-Header: CustomValue ``` ## Practical note Use `CustomHeader` for app-specific or infrastructure-specific headers. If you later call `allowlist_headers(...)`, remember to allow the custom name explicitly when needed. ## Best Practices - Prefer standard header names when they exist; use custom names only for application- or infrastructure-specific behavior. - If you use `allowlist_headers(...)`, remember that custom names may need to be added through `allow_extra=...`. ## Configuration with `Secure` Use `CustomHeader` when you need a header without a dedicated builder. You can set the name and value directly, then update the value later if needed. ### Methods Available - **`set(value)` / `value(value)`**: Updates the value of the custom header. - **`header_value`**: Property that retrieves the current value of the custom header. ## Example Usage To define a custom header and use it in a secure configuration: ```python from secure import CustomHeader custom_header = CustomHeader("X-Custom-Header", "CustomValue") print(custom_header.header_name) # Output: 'X-Custom-Header' print(custom_header.header_value) # Output: 'CustomValue' # Update the value custom_header.set("NewValue") print(custom_header.header_value) # Output: 'NewValue' ``` ## **Resources** - [MDN Web Docs: HTTP Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) ## **Attribution** This library implements security recommendations from trusted sources: - [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)) - [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) ================================================ FILE: docs/headers/dns_prefetch_control.md ================================================ # X-DNS-Prefetch-Control ## What it does `X-DNS-Prefetch-Control` controls **DNS prefetching**, where browsers may proactively resolve domain names for links and referenced subresources (images, CSS, JS, etc.) in the background to reduce perceived latency. ## Minimal example ```python from secure import Secure, XDnsPrefetchControl secure_headers = Secure( xdfc=XDnsPrefetchControl().off() ) ``` ## Resulting header ```http X-DNS-Prefetch-Control: off ``` ## Practical note When this header is absent, supporting browsers commonly behave as if DNS prefetching is on. Add the header only when you want to state a clear preference. ## Default behavior If you create `XDnsPrefetchControl()` and do not set a directive, it returns the library default value: - **Default header value:** `off` > Note (MDN behavior): In browsers that support DNS prefetching, if this header is **not present**, the effective behavior is typically **`on`**. This library’s default is **privacy-first** when you choose to emit the header. ## Using with `Secure` If you don’t configure anything, the default value is emitted. `Preset.BASIC` includes `X-DNS-Prefetch-Control: off`; `Preset.BALANCED` and `Preset.STRICT` leave it out unless you add it explicitly. ## Common recipes ### 1) Disable DNS prefetching (recommended when you don’t control outbound links) ```python from secure import XDnsPrefetchControl xdfc = XDnsPrefetchControl() # default: off print(xdfc.header_name) # X-DNS-Prefetch-Control print(xdfc.header_value) # off ``` ### 2) Enable DNS prefetching ```python xdfc = XDnsPrefetchControl().on() print(xdfc.header_value) # on ``` ### 3) Backwards-compatible builder names If you prefer the older API vocabulary: ```python xdfc = XDnsPrefetchControl().allow() # == .on() xdfc = XDnsPrefetchControl().disable() # == .off() ``` ## Builder API ### Canonical directives - `.on()` Enables DNS prefetching (commonly the effective behavior when the header is absent in supporting browsers). - `.off()` Disables DNS prefetching (useful to reduce information leakage to third-party domains). ### Backwards-compatible aliases - `.allow()` → same as `.on()` - `.disable()` → same as `.off()` ## Escape hatches ### `.value("...")` / `.set("...")` Set an explicit header value (replaces the current value): ```python xdfc = XDnsPrefetchControl().value("off") print(xdfc.header_value) # off ``` If you pass `ON` / `Off` (any casing), the builder normalizes to `on` / `off` for stable output. ### `.custom("token")` Set a **non-standard / non-MDN** token (escape hatch): ```python xdfc = XDnsPrefetchControl().custom("off") print(xdfc.header_value) # off ``` (For this header, non-`on`/`off` values are unusual, but the escape hatch exists for consistency across the library.) ### `.clear()` Reset to the library default: ```python xdfc = XDnsPrefetchControl().on().clear() print(xdfc.header_value) # off ``` ## Deterministic output & overwrites - Output is always a **single token** (`on` or `off`) when using `.on()` / `.off()` (stable and deterministic). - Setting the value multiple times overwrites the previous value (last call wins). - `.set(...)`, `.value(...)`, and `.custom(...)` reject CR/LF; `Secure.validate_and_normalize_headers(...)` performs the broader normalization pass. ## Compatibility notes - This header is **non-standard**. - Browser behavior differs across engines and versions; treat this as a best-effort control rather than a guaranteed security boundary. ## Attribution This library implements security recommendations and behavior described by: - [MDN Web Docs: X-DNS-Prefetch-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-DNS-Prefetch-Control) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)) - [OWASP Secure Headers Project: X-DNS-Prefetch-Control](https://owasp.org/www-project-secure-headers/#x-dns-prefetch-control) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) ================================================ FILE: docs/headers/permissions_policy.md ================================================ # Permissions-Policy ## What it does The `Permissions-Policy` HTTP response header lets you enable or disable access to selected browser features and powerful APIs in the current document and in nested browsing contexts (iframes). It replaces the deprecated `Feature-Policy` header. In this library, `PermissionsPolicy` is a fluent builder for producing a single `Permissions-Policy` header value, suitable for applying via `Secure`. ## Minimal example ```python from secure import PermissionsPolicy, Secure secure_headers = Secure( permissions=PermissionsPolicy() .geolocation() .microphone() .camera() ) ``` ## Resulting header ```http Permissions-Policy: geolocation=(), microphone=(), camera=() ``` ## Practical note Browser support varies by feature. Keep the policy restrictive, then test the specific features your app actually needs in real browsers. ## Best practices - Start restrictive: disable features you don’t need to reduce attack surface and protect privacy. - Enable selectively: allow features only where required, and only for trusted origins. - Validate in real browsers: support varies by feature and browser; test the behaviors you rely on. ## Configuration with `Secure` `Preset.BALANCED` and `Preset.STRICT` include `geolocation=(), microphone=(), camera=()` by default; `Preset.BASIC` does not add `Permissions-Policy`. ## Allowlist syntax `PermissionsPolicy` uses MDN-style allowlist syntax for each directive: - **No tokens** → `()` (feature disabled) - **`"*"`** → `*` (feature allowed everywhere; must be used alone) - **`"self"` / `"src"`** → tokens for same-origin / iframe source origin - **Origins** → pass a URL (e.g. `"https://a.example.com"`); it is emitted as a double-quoted origin in the header value Examples: ```python policy = ( PermissionsPolicy() .geolocation("*") # geolocation=* .camera("self", "https://a.example.com") # camera=(self "https://a.example.com") .microphone() # microphone=() ) print(policy.header_value) ``` ## Methods Common methods you’ll use: - **`geolocation(*allowlist)`**, **`camera(*allowlist)`**, **`microphone(*allowlist)`**, etc.: configure specific directives. - **`add_directive(directive, *allowlist)`** (alias: **`directive(...)`**): set any directive by name (future-proof when browsers add new ones). - **`value(raw)`** (alias: **`set(raw)`**): set a complete prebuilt header value (escape hatch; bypasses directive building). - **`clear()`**: remove all configured directives and any raw override. ## Example usage ```python from secure import PermissionsPolicy, Secure permissions_policy = ( PermissionsPolicy() .geolocation() # disabled .camera("self", "https://a.example.com") .microphone("self") ) print(permissions_policy.header_name) # 'Permissions-Policy' print(permissions_policy.header_value) # 'geolocation=(), camera=(self "https://a.example.com"), microphone=(self)' secure_headers = Secure(permissions=permissions_policy) ``` ## Resources - [MDN Web Docs: Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy) - [OWASP Secure Headers Project: Permissions-Policy](https://owasp.org/www-project-secure-headers/#permissions-policy) ## Attribution This library implements security recommendations from trusted sources: - MDN Web Docs (licensed under CC-BY-SA 2.5) - OWASP Secure Headers Project (licensed under CC-BY-SA 4.0) ================================================ FILE: docs/headers/referrer_policy.md ================================================ # Referrer-Policy ## What it does The `Referrer-Policy` response header controls how much referrer information (sent via the `Referer` header) is included with outgoing requests. It is primarily a privacy and data-minimization control, with important security implications when navigating across origins or downgrading from HTTPS to HTTP. > Note: `Referer` is intentionally misspelled in HTTP. `Referrer-Policy` does **not** share that misspelling. ## Minimal example ```python from secure import ReferrerPolicy, Secure secure_headers = Secure( referrer=ReferrerPolicy().strict_origin_when_cross_origin() ) ``` ## Resulting header ```http Referrer-Policy: strict-origin-when-cross-origin ``` ## Practical note `strict-origin-when-cross-origin` is the recommended default because it preserves same-origin behavior while avoiding full URL leakage across origins. Move to `no-referrer` only when you want the strictest privacy posture. ## Default behavior **Default header value:** `strict-origin-when-cross-origin` This matches modern browser defaults: if no policy is specified (or the provided value is invalid), the effective policy is `strict-origin-when-cross-origin`. ## Best practices (recommended choices) - **`strict-origin-when-cross-origin` (recommended default)** Sends the full referrer (origin + path + query) for same-origin requests; sends **origin only** for cross-origin HTTPS→HTTPS; sends **no `Referer`** when downgrading (HTTPS→HTTP). - **`no-referrer` (max privacy)** Omits the `Referer` header entirely for all requests. - **`same-origin` (strict privacy across sites)** Sends referrer only for same-origin requests; omits it for cross-origin requests. - **Avoid `unsafe-url`** unless you fully understand the impact (it can leak sensitive URL data across origins and to insecure destinations). ## Configuration with `Secure` ```python from secure import ReferrerPolicy, Secure secure_headers = Secure( referrer=ReferrerPolicy() # uses the default: strict-origin-when-cross-origin ) ``` `Preset.BALANCED` uses `strict-origin-when-cross-origin`; `Preset.BASIC` and `Preset.STRICT` use `no-referrer`. ### Set a single explicit policy Use `value(...)` (or `custom(...)`) when you want to **replace** any configured policies and set exactly one value: ```python from secure import ReferrerPolicy, Secure secure_headers = Secure( referrer=ReferrerPolicy().value("no-referrer") ) ``` You can also use the fluent directive helpers: ```python secure_headers = Secure( referrer=ReferrerPolicy().no_referrer() ) ``` ### Specify a fallback policy list (HTTP header only) Browsers support a **comma-separated list** in the `Referrer-Policy` HTTP header. The desired (most modern) policy should be listed **last**. ```python from secure import ReferrerPolicy rp = ReferrerPolicy().fallback("no-referrer", "strict-origin-when-cross-origin") print(rp.header_name) # Referrer-Policy print(rp.header_value) # no-referrer, strict-origin-when-cross-origin ``` You can build the same list with `.add(...)`: ```python rp = ( ReferrerPolicy() .clear() .add("no-referrer") .add("strict-origin-when-cross-origin") ) ``` > Note: the fallback _list_ behavior is supported in the HTTP header, but not in the HTML `referrerpolicy` attribute. ## API reference (ReferrerPolicy) ### Core builder methods - `value("...")` / `custom("...")` Replace all configured policies with the provided value (supports comma-separated lists). - `add("...")` / `set("...")` Append one or more policy tokens (supports comma-separated lists). Duplicate tokens are ignored. - `fallback(*policies)` Replace the current policies with an explicit ordered fallback list. - `clear()` Clear configured policies (returns to default behavior unless you add values afterward). ### Directive helpers (MDN policies) Each of these appends the corresponding token (same behavior as `add("token")`): - `no_referrer()` → `no-referrer` Omits the `Referer` header entirely. - `no_referrer_when_downgrade()` → `no-referrer-when-downgrade` Sends full referrer for same-or-more secure requests; omits referrer on downgrade (HTTPS→HTTP). - `origin()` → `origin` Sends only the origin (scheme + host + port). - `origin_when_cross_origin()` → `origin-when-cross-origin` Same-origin: full referrer; cross-origin and downgrade: origin only. - `same_origin()` → `same-origin` Same-origin: full referrer; cross-origin: omit referrer. - `strict_origin()` → `strict-origin` Sends only origin for same-security requests; omits on downgrade (HTTPS→HTTP). - `strict_origin_when_cross_origin()` → `strict-origin-when-cross-origin` Same-origin: full referrer; cross-origin HTTPS→HTTPS: origin only; downgrade: omit. - `unsafe_url()` → `unsafe-url` Sends origin + path + query for all requests (generally discouraged; may leak sensitive data). ## Example usage ```python from secure import ReferrerPolicy referrer_policy = ReferrerPolicy().strict_origin_when_cross_origin() print(referrer_policy.header_name) # 'Referrer-Policy' print(referrer_policy.header_value) # 'strict-origin-when-cross-origin' ``` ## Resources - MDN Web Docs: Referrer-Policy [https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Referrer-Policy) - OWASP Secure Headers Project: Referrer-Policy [https://owasp.org/www-project-secure-headers/#referrer-policy](https://owasp.org/www-project-secure-headers/#referrer-policy) ## Attribution This library implements security recommendations from trusted sources: - MDN Web Docs (licensed under CC-BY-SA 2.5) [https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor](https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor) [https://creativecommons.org/licenses/by-sa/2.5/](https://creativecommons.org/licenses/by-sa/2.5/) - OWASP Secure Headers Project (licensed under CC-BY-SA 4.0) [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) ================================================ FILE: docs/headers/server.md ================================================ # Server Header ## What it does The `Server` header can reveal details about the software handling the request. In `secure`, the builder defaults to an empty string so your application can avoid adding identifying detail when the surrounding stack allows it. ## Minimal example ```python from secure import Secure, Server secure_headers = Secure( server=Server().set("") ) ``` ## Resulting header ```http Server: ``` ## Practical note Application code can only control this header if the surrounding stack does not re-add its own value. Check your ASGI server, WSGI server, proxy, or CDN settings too. ## Best Practices - **Set an empty value or custom string**: Use an empty or generic value when you want `secure` to control the header. - **Avoid exposing server information**: Avoid leaving the default server response, which may expose sensitive version information. - **Check upstream defaults**: Proxies, ASGI servers, and framework middleware may still add their own `Server` header unless you disable that behavior. ## Configuration with `Secure` Use `Server` to control the `Server` header value. Its default value is an empty string. ### Methods Available - **`set(value)`**: Set a custom value for the `Server` header. - **`clear()`**: Clear any custom value and revert the header to its default secure value (an empty string). ## Example Usage To set up the `Server` header and hide the server information: ```python from secure import Server server_header = Server().set("") print(server_header.header_name) # Output: 'Server' print(server_header.header_value) # Output: '' ``` Then pass it into `Secure`: ```python from secure import Secure secure_headers = Secure(server=server_header) ``` ### Special Considerations for Frameworks Some frameworks like Uvicorn automatically inject a `Server` header. If you're using Uvicorn and need to override or remove this header, refer to the [framework integration guide](../frameworks.md) for specific instructions on how to disable Uvicorn's default `Server` header. ## **Resources** - [MDN Web Docs: Server Header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server) - [OWASP Secure Headers Project: Server Header](https://owasp.org/www-project-secure-headers/#server-header) ## **Attribution** This library implements security recommendations from trusted sources: - [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)) - [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#server-header) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) ================================================ FILE: docs/headers/strict_transport_security.md ================================================ # Strict-Transport-Security (HSTS) ## What it does The `Strict-Transport-Security` (HSTS) header tells browsers that a host **must only be accessed over HTTPS**. Once a browser has received this header, it will automatically upgrade future HTTP navigations to HTTPS for the configured duration, helping prevent man-in-the-middle and downgrade attacks. > Important: Browsers **ignore** `Strict-Transport-Security` if it is delivered over **insecure HTTP**. You must send it over HTTPS only. ## Minimal example ```python from secure import Secure, StrictTransportSecurity secure_headers = Secure( hsts=StrictTransportSecurity().max_age(31536000).include_subdomains() ) ``` ## Resulting header ```http Strict-Transport-Security: max-age=31536000; includeSubDomains ``` ## Practical note Do not enable `includeSubDomains` until every subdomain is HTTPS-ready. Treat `preload()` as a deliberate rollout step because removal is slow once a domain is on the preload list. ## Default behavior If you do not configure any directives, this library emits the default header value: - **Default header value:** `max-age=31536000` (one year) ## Best practices - **Use a long `max-age`**: One year (`31536000` seconds) is a common baseline. - **Include subdomains (carefully)**: Add `includeSubDomains` only if _all_ subdomains are HTTPS-ready. - **Only use `preload` when you mean it**: - `preload` is intended for submitting your domain to the HSTS preload list. - When using `preload`, the library enforces MDN’s requirements: - `max-age` must be **at least 31536000** - `includeSubDomains` must be present ## Configuration with `Secure` Use `StrictTransportSecurity` for fluent, chainable configuration. `Preset.BASIC` and `Preset.BALANCED` use one year with `includeSubDomains`; `Preset.STRICT` uses two years with `includeSubDomains`. ### Preload configuration If you opt into preload, the library ensures preload requirements are satisfied: ```python from secure import StrictTransportSecurity hsts = ( StrictTransportSecurity() .max_age(31536000) .include_subdomains() .preload() ) print(hsts.header_name) # 'Strict-Transport-Security' print(hsts.header_value) # 'max-age=31536000; includeSubDomains; preload' ``` If `preload()` is enabled with a `max-age` less than `31536000`, the header builder will raise a `ValueError`. ## Methods available - **`max_age(seconds)`** Set `max-age`: how long (in seconds) the browser should remember to only use HTTPS for this host. - **`include_subdomains()`** Add `includeSubDomains`: apply the HSTS policy to all subdomains as well. - **`preload()`** Add `preload`: indicates intent to meet HSTS preload requirements. This library: - automatically enables `includeSubDomains` - enforces `max-age >= 31536000` - **`clear()`** Clear configured directives and reset back to the library default behavior. - **`value(str)` / `set(str)`** Escape hatch: set a raw header value (replaces any configured directives). The value must not contain CR/LF characters. ## Example usage Minimal one-year HSTS: ```python from secure import StrictTransportSecurity hsts = StrictTransportSecurity().max_age(31536000) print(hsts.header_value) # 'max-age=31536000' ``` One-year HSTS including subdomains: ```python from secure import StrictTransportSecurity hsts = StrictTransportSecurity().max_age(31536000).include_subdomains() print(hsts.header_value) # 'max-age=31536000; includeSubDomains' ``` ## Resources - MDN Web Docs: Strict-Transport-Security [https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Strict-Transport-Security) - OWASP Secure Headers Project [https://owasp.org/www-project-secure-headers/](https://owasp.org/www-project-secure-headers/) - HSTS Preload List [https://hstspreload.org/](https://hstspreload.org/) ## Attribution This library implements security recommendations from trusted sources: - MDN Web Docs: Strict-Transport-Security (licensed under CC-BY-SA 2.5) [https://creativecommons.org/licenses/by-sa/2.5/](https://creativecommons.org/licenses/by-sa/2.5/) - OWASP Secure Headers Project (licensed under CC-BY-SA 4.0) [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) ================================================ FILE: docs/headers/x-permitted-cross-domain-policies.md ================================================ # X-Permitted-Cross-Domain-Policies ## What it does `X-Permitted-Cross-Domain-Policies` is a **response header** that sets a _meta-policy_ controlling whether site resources can be accessed cross-origin by documents running in legacy web clients (for example, Adobe Acrobat or Microsoft Silverlight). Usage is less common today because Flash/Silverlight have been deprecated, but many security testing tools still check for `X-Permitted-Cross-Domain-Policies: none` to reduce the risk of an overly-permissive cross-domain policy being present accidentally or maliciously. > This documentation format mirrors the style used in the existing header docs (e.g., Cache-Control). ## Minimal example ```python from secure import Secure, XPermittedCrossDomainPolicies secure_headers = Secure( xpcdp=XPermittedCrossDomainPolicies().none() ) ``` ## Resulting header ```http X-Permitted-Cross-Domain-Policies: none ``` ## Practical note Most modern apps do not need this header, but security scanners still look for it. Add it when you want an explicit deny policy for legacy cross-domain policy files. ## Default behavior If you create `XPermittedCrossDomainPolicies()` and do not set a policy, it returns the library default value: - **Default header value:** `none` This is the least permissive option and is the most common secure setting when you do not need legacy cross-domain policy behavior. ## Using with `Secure` If you don’t configure anything, the default value is emitted. `Preset.BASIC` includes `X-Permitted-Cross-Domain-Policies: none`; `Preset.BALANCED` and `Preset.STRICT` leave it out unless you add it explicitly. ## Common recipes ### 1) Disallow cross-domain policy files (recommended default) ```python from secure import XPermittedCrossDomainPolicies xpcdp = XPermittedCrossDomainPolicies() # default: none print(xpcdp.header_name) # X-Permitted-Cross-Domain-Policies print(xpcdp.header_value) # none ``` MDN notes this is the typical configuration when you don’t need legacy clients. ### 2) Allow only a master policy file ```python xpcdp = XPermittedCrossDomainPolicies().master_only() print(xpcdp.header_value) # master-only ``` This allows cross-domain access to the master policy file defined on the same domain. ### 3) Constrain policy files by content type (HTTP/HTTPS only) ```python xpcdp = XPermittedCrossDomainPolicies().by_content_type() print(xpcdp.header_value) # by-content-type ``` Only policy files served with `Content-Type: text/x-cross-domain-policy` are allowed. ### 4) Indicate this response should not be treated as a policy file ```python xpcdp = XPermittedCrossDomainPolicies().none_this_response() print(xpcdp.header_value) # none-this-response ``` This directive is unique to the HTTP header and indicates the current document should not be used as a policy file. ## Builder API ### Policy directives (single value) - `.none()` - `.master_only()` - `.by_content_type()` (HTTP/HTTPS only) - `.by_ftp_filename()` (FTP only) - `.all()` - `.none_this_response()` (HTTP-header-only) These map directly to the directive definitions described by MDN (and largely echoed by OWASP). ### Typed helper - `.policy("none" | "master-only" | "by-content-type" | "by-ftp-filename" | "all" | "none-this-response")` Raises `ValueError` for unsupported values (helps catch typos early). ## Escape hatches ### `.value("...")` Set an explicit header value (replaces any configured directive): ```python xpcdp = XPermittedCrossDomainPolicies().value("none") print(xpcdp.header_value) # none ``` ### `.custom("token")` Alias for `.value(...)` (use when you intentionally want a raw string value): ```python xpcdp = XPermittedCrossDomainPolicies().custom("master-only") print(xpcdp.header_value) # master-only ``` ### `.clear()` Reset to the default: ```python xpcdp = XPermittedCrossDomainPolicies().all().clear() print(xpcdp.header_value) # none ``` ## Deterministic output & safety - The header value is rendered as a **single directive token**, so output is inherently deterministic. - Raw setters (`.value(...)` / `.custom(...)`) normalize obvious header-splitting primitives (CR/LF) before serialization; stricter validation can be enforced via `Secure.validate_and_normalize_headers(...)`. ## Attribution This library implements security recommendations and behavior described by: - [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Permitted-Cross-Domain-Policies) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)) - [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/#x-permitted-cross-domain-policies) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) ================================================ FILE: docs/headers/x_content_type_options.md ================================================ # X-Content-Type-Options ## What it does The `X-Content-Type-Options` header tells browsers to **respect the MIME type declared in `Content-Type`** instead of trying to guess ("sniff") a different type. In practice, setting `X-Content-Type-Options: nosniff` can cause browsers to **block**: - `style` requests not served as `text/css` - `script` requests not served with a JavaScript MIME type This helps reduce the risk of content being interpreted as executable when it should not be. ## Minimal example ```python from secure import Secure, XContentTypeOptions secure_headers = Secure( xcto=XContentTypeOptions().nosniff(), ) ``` ## Resulting header ```http X-Content-Type-Options: nosniff ``` ## Practical note `nosniff` can expose incorrect MIME types in your app or asset pipeline. If enabling it breaks assets, fix the response `Content-Type` rather than weakening the header. ## Best Practices - **Set to `nosniff`** (recommended): This is the standard and widely supported directive. - **Use correct `Content-Type` values**: `nosniff` is most effective when your server sends accurate MIME types. ## Configuration with `Secure` The `XContentTypeOptions` class configures `X-Content-Type-Options`. **Default header value:** `nosniff` All built-in presets include it. ### Methods available - **`nosniff()`**: Sets the header to `nosniff`, which blocks certain `script`/`style` requests when MIME types are incorrect. - **`set(value)` / `value(value)`**: Sets a raw/custom header value (escape hatch). `value` is an alias for `set`. - **`clear()`**: Resets the header to the library default (`nosniff`). > Note: `set/value` are escape hatches. If you use `Secure.validate_and_normalize_headers(...)`, that layer is responsible for sanitization and safety checks. ## Example usage ```python from secure import XContentTypeOptions xcto = XContentTypeOptions().nosniff() print(xcto.header_name) # 'X-Content-Type-Options' print(xcto.header_value) # 'nosniff' ``` Apply via `Secure`: ```python from secure import Secure, XContentTypeOptions secure_headers = Secure(xcto=XContentTypeOptions().nosniff()) ``` ## Resources - MDN Web Docs: X-Content-Type-Options [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) - OWASP Secure Headers Project: X-Content-Type-Options [https://owasp.org/www-project-secure-headers/#x-content-type-options](https://owasp.org/www-project-secure-headers/#x-content-type-options) ## Attribution This library implements security recommendations from trusted sources: - MDN Web Docs (CC-BY-SA 2.5) [https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor](https://developer.mozilla.org/en-US/docs/MDN/Community/Roles_teams#contributor) [https://creativecommons.org/licenses/by-sa/2.5/](https://creativecommons.org/licenses/by-sa/2.5/) - OWASP Secure Headers Project (CC-BY-SA 4.0) [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) ================================================ FILE: docs/headers/x_frame_options.md ================================================ # X-Frame-Options ## What it does `X-Frame-Options` is an HTTP **response header** that tells supporting browsers whether a page is allowed to render inside a ``, `