diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a1dd730..4d35c13 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,5 +21,9 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} + - name: Install toolchain + run: pip install ruff - name: Unit tests run: python -m unittest tests/*/*.py + - name: Lint + run: ruff check secure tests diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a9112..4475853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Placeholder for upcoming changes. +## [2.0.0] - 2025-12-13 + +### Breaking Changes + +- The `Secure` API now requires Python 3.10+ and uses the new builder-style header modules with full typing; this release replaces the previous legacy surface and removes the older cookie-centric helpers. + +### Added + +- Comprehensive validation pipeline helpers (`allowlist_headers`, `deduplicate_headers`, `validate_and_normalize_headers`) and typed presets for `Secure`. +- New header builder coverage for modern headers (CSP, Permissions Policy, COEP, etc.) with deterministic outputs. +- Async-safe `set_headers_async` support for both method-call and mapping-style response objects plus helper mocks and contract tests. + +### Testing + +- Added full contract tests for the header builders along with end-to-end coverage for `Secure` usage and response integration. + +### Docs + +- Expanded README with usage examples, advanced pipeline guidance, and updated framework integration references. + ## [1.0.1] - 2024-10-18 ### Fixed diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..4814784 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,38 @@ +# 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/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e9a81fe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# 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 ruff + ``` + _Optional:_ `uv` is the package manager used by the project for releases; you can use `uv add ...` to manage dependencies, but it is not required for local development. + +## Running tests, linting, and formatting + +- **Run unit tests:** `python -m unittest tests/*/*.py` +- **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 `python -m unittest tests/*/*.py` 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). diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 7518d90..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,2 +0,0 @@ -include LICENSE -exclude tests/* \ No newline at end of file diff --git a/README.md b/README.md index 3a17b31..9557170 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,56 @@ -# secure.py +# secure -_A simple, yet powerful way to secure your Python web applications across multiple frameworks._ +A small, focused library for adding modern security headers to Python web applications. [![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/) -[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![Downloads](https://pepy.tech/badge/secure)](https://pepy.tech/project/secure) [![License](https://img.shields.io/pypi/l/secure.svg)](https://github.com/TypeError/secure/blob/main/LICENSE) [![GitHub Stars](https://img.shields.io/github/stars/TypeError/secure.svg)](https://github.com/TypeError/secure/stargazers) -## **Introduction** +--- + +## Introduction + +Security headers are one of the simplest ways to raise the security bar for a web application, but they are often applied inconsistently across frameworks and deployments. + +`secure` gives you a single, modern, well typed API for configuring and applying HTTP security headers in Python. It focuses on: -In today's web landscape, security is paramount. **secure.py** is a lightweight Python library designed to effortlessly add **security headers** to your web applications, protecting them from common vulnerabilities. Whether you're using **Django**, **Flask**, **FastAPI**, or any other popular framework, `secure.py` provides a unified API to enhance your application's security posture. +- Good defaults that are safe to adopt. +- A small, explicit API instead of a large framework. +- Support for both synchronous and asynchronous response objects. +- Framework agnostic integration so you can use the same configuration everywhere. + +The package is published on PyPI as `secure` and imported with: + +```python +import secure +``` --- -## **Why Use secure.py?** +## Why use `secure` -- 🔒 **Apply Essential Security Headers**: Implement headers like CSP, HSTS, and more with minimal effort. -- 🛠️ **Consistent API Across Frameworks**: A unified approach for different web frameworks. -- ⚙️ **Customizable with Secure Defaults**: Start secure out-of-the-box and customize as needed. -- 🚀 **Easy Integration**: Compatible with Python's most-used frameworks. -- 🐍 **Modern Pythonic Design**: Leverages Python 3.10+ features for cleaner and more efficient code. +- Apply essential security headers with a few lines of code. +- Share one configuration across multiple frameworks and applications. +- Start from secure presets, then customize as your needs grow. +- Keep header logic out of your views and handlers. +- Use one library for FastAPI, Starlette, Flask, Django, and more. +- Rely on modern Python 3.10+ features and full type hints for better editor support. + +If you want your app to ship with a strong security baseline without pulling in a heavyweight dependency, `secure` is designed for you. --- -## **Supported Frameworks** +## Supported frameworks -**secure.py** supports the following Python web frameworks: +`secure` integrates with a range of popular Python web frameworks. The core API is framework independent, and each framework uses the same `Secure` object and methods. | Framework | Documentation | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | [aiohttp](https://docs.aiohttp.org) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#aiohttp) | | [Bottle](https://bottlepy.org) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#bottle) | | [CherryPy](https://cherrypy.dev/) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#cherrypy) | +| [Dash](https://dash.plotly.com/) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#dash) | | [Django](https://www.djangoproject.com) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#django) | | [Falcon](https://falconframework.org) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#falcon) | | [FastAPI](https://fastapi.tiangolo.com) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#fastapi) | @@ -44,190 +61,438 @@ In today's web landscape, security is paramount. **secure.py** is a lightweight | [Quart](https://quart.palletsprojects.com/en/latest/) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#quart) | | [Responder](https://responder.kennethreitz.org/) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#responder) | | [Sanic](https://sanicframework.org) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#sanic) | +| [Shiny](https://shiny.posit.co/py/) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#shiny) | | [Starlette](https://www.starlette.io/) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#starlette) | | [Tornado](https://www.tornadoweb.org/) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#tornado) | | [TurboGears](https://turbogears.org/) | [Integration Guide](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#turbogears) | --- -## **Features** +## Features + +- **Secure headers** + Apply headers like `Strict-Transport-Security`, `Content-Security-Policy`, `X-Content-Type-Options`, `X-Frame-Options`, and more. + +- **Presets with secure defaults** + Start from opinionated presets like `Preset.BASIC` and `Preset.STRICT`, then customize as needed. + +- **Policy builders** + Compose complex policies such as CSP and Permissions Policy through a fluent API. + +- **Framework agnostic** + Works with sync and async response objects and does not depend on any single framework. -- 🔒 **Secure Headers**: Automatically apply headers like `Strict-Transport-Security`, `X-Frame-Options`, and more. -- 🛠️ **Customizable Policies**: Flexibly build your own security policies using method chaining. -- 🌐 **Framework Integration**: Compatible with various frameworks, ensuring cross-compatibility. -- 🚀 **No External Dependencies**: Lightweight and easy to include in any project. -- 🧩 **Easy to Use**: Integrate security headers in just a few lines of code. -- ⚡ **Asynchronous Support**: Async support for modern frameworks like **FastAPI** and **Starlette**. -- 📝 **Enhanced Type Hinting**: Complete type annotations for better developer experience. -- 📚 **Attribution to Trusted Sources**: Implements recommendations from MDN and OWASP. +- **Zero external dependencies** + Easy to audit and suitable for security sensitive environments. + +- **Modern Python design** + Uses Python 3.10+ features and full type hints so your editor and type checker can help you. --- -## **Requirements** +## Requirements + +- **Python 3.10 or higher** -- **Python 3.10** or higher + `secure` targets modern Python and is currently tested on Python 3.10 through 3.13. - This library leverages modern Python features introduced in Python 3.10 and 3.11, such as: + It uses features introduced in Python 3.10, including: - - **Union Type Operator (`|`)**: Simplifies type annotations. - - **Structural Pattern Matching (`match` statement)**: Enhances control flow. - - **Improved Type Hinting and Annotations**: Provides better code clarity and maintenance. - - **`cached_property`**: Optimize memory usage and performance. + - Union type operator (`|`) for cleaner type annotations. + - Structural pattern matching (`match`). + - Improved typing and annotations. + - `functools.cached_property` for efficient lazy computation. - **Note:** If you're using an older version of Python (3.6 to 3.9), please use version **0.3.0** of this library, which maintains compatibility with those versions. + If you need support for Python 3.6 through 3.9, use version `0.3.0` of the library. - **Dependencies** - This library has no external dependencies outside of the Python Standard Library. + This library has no external dependencies outside of the Python standard library. --- -## **Installation** - -You can install secure.py using pip, pipenv, or poetry: - -**pip**: +## Installation -```bash -pip install secure -``` +You can install `secure` with your preferred Python package manager. -**Pipenv**: +### Using `uv` ```bash -pipenv install secure +uv add secure ``` -**Poetry**: +### Using `pip` ```bash -poetry add secure +pip install secure ``` --- -## **Getting Started** - -Once installed, you can quickly integrate `secure.py` into your project: +## Quick start -### Synchronous Usage +The core entry point is the `Secure` class. A typical simple setup looks like this: ```python import secure -# Initialize secure headers with default settings secure_headers = secure.Secure.with_default_headers() -# Apply the headers to your framework response object +# For a synchronous framework secure_headers.set_headers(response) + +# For an asynchronous framework +await secure_headers.set_headers_async(response) ``` -### Asynchronous Usage +`Secure.with_default_headers()` is equivalent to `Secure.from_preset(Preset.BALANCED)`, the recommended default profile. + +`set_headers` and `set_headers_async` both operate on a response object that either: + +- Exposes a `set_header(name, value)` method, or +- Exposes a mutable `headers` mapping that supports item assignment. -For frameworks like **FastAPI** and **Starlette** that support asynchronous operations, use the async method: +If your framework uses a different contract, see the framework specific guides or use `header_items()` to apply headers manually. + +## Middleware + +`secure.middleware` re-exports `SecureWSGIMiddleware` and `SecureASGIMiddleware`. Each middleware accepts a `Secure` instance (defaulting to `Secure.with_default_headers()`), overwrites headers by default, and only appends duplicates when a normalized name is included in `multi_ok` (the default `secure.MULTI_OK` includes `Content-Security-Policy`). + +### WSGI (Flask + Django) + +Wrap any WSGI stack with `SecureWSGIMiddleware`, and pass a configured `Secure` instance if you need a custom CSP or additional headers. ```python -import secure +from flask import Flask +from secure import Secure +from secure.middleware import SecureWSGIMiddleware -# Initialize secure headers with default settings -secure_headers = secure.Secure.with_default_headers() +secure_headers = Secure.with_default_headers() +app = Flask(__name__) +app.wsgi_app = SecureWSGIMiddleware(app.wsgi_app, secure=secure_headers) +``` -# Apply the headers asynchronously to your framework response object -await secure_headers.set_headers_async(response) +For Django, apply the headers through a middleware class since Django’s middleware pipeline wraps requests and responses rather than the raw WSGI callable: + +```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 ``` -### **Example Usage** +Register the class in your `MIDDLEWARE` setting to enforce security headers on every response. + +### ASGI (FastAPI + Shiny for Python) + +`SecureASGIMiddleware` modifies only HTTP scopes (WebSocket messages pass through untouched). Mount it manually or via FastAPI’s `add_middleware`, and pass any `Secure` instance if you need to adjust the defaults. ```python -import secure +from fastapi import FastAPI +from secure import Secure +from secure.middleware import SecureASGIMiddleware -# Create a Secure instance with default headers -secure_headers = secure.Secure.with_default_headers() +secure_headers = Secure.with_default_headers() +app = FastAPI() +app.add_middleware(SecureASGIMiddleware, secure=secure_headers) +``` -# Apply default secure headers to a response object -secure_headers.set_headers(response) +If you need to tailor the CSP, build a custom `Secure` instance before wiring the middleware: + +```python +from secure import ContentSecurityPolicy + +secure_headers = Secure( + csp=ContentSecurityPolicy().default_src("'self'").script_src("https://trusted.cdn") +) +app = SecureASGIMiddleware(app, secure=secure_headers) +``` + +Shiny for Python apps can be wrapped in the same way: + +```python +from shiny import App +from secure import Secure +from secure.middleware import SecureASGIMiddleware + +secure_headers = Secure.with_default_headers() +app = SecureASGIMiddleware(App(), secure=secure_headers) ``` +### Customizing `multi_ok` + +Pass the `multi_ok` argument to either middleware to append additional occurrences of headers that must appear multiple times (for example, when downstream code already emits a `Content-Security-Policy` line). + --- -## **Default Secure Headers** +## Default secure headers -By default, `secure.py` applies the following headers when using `with_default_headers()`: +When you call `Secure.with_default_headers()` (or `Secure.from_preset(Preset.BALANCED)`), `secure` configures the recommended defaults that balance security and usability: ```http -Cache-Control: no-store Cross-Origin-Opener-Policy: same-origin -Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none' -Strict-Transport-Security: max-age=31536000 +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: X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN ``` +These defaults limit cross origin data leaks, mitigate clickjacking and MIME sniffing, and enforce a conservative Content Security Policy you can extend later. Balanced omits `Cache-Control` as well as the legacy/compatibility headers (`X-Permitted-Cross-Domain-Policies`, `X-DNS-Prefetch-Control`, `Origin-Agent-Cluster`, `X-Download-Options`, `X-XSS-Protection`), so add them manually if your deployment still depends on them. + +--- + +## Presets + +If you prefer to think in terms of profiles instead of individual headers, `secure` provides presets via the `Preset` enum and `Secure.from_preset`. + +```python +from secure import Preset, Secure + +# Recommended defaults for most applications +balanced_headers = Secure.from_preset(Preset.BALANCED) + +# Helmet-parity defaults for compatibility-focused setups +basic_headers = Secure.from_preset(Preset.BASIC) + +# Hardened defaults for security-focused deployments +strict_headers = Secure.from_preset(Preset.STRICT) +``` + +### BALANCED preset + +The `BALANCED` preset is the new recommended default and matches `Secure.with_default_headers()`. It balances security with compatibility while keeping response headers relatively tight: + +```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 +Server: +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +``` + +Balanced omits `Cache-Control` and the legacy/resource headers included by `Preset.BASIC`, but you can still add them manually if your deployment relies on them. + +### BASIC preset + +The `BASIC` preset matches Helmet.js defaults and ships with a broader compatibility-focused header set. It is useful when you require the same collection of headers Helmet enables out of the box: + +```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 +Referrer-Policy: no-referrer +X-Permitted-Cross-Domain-Policies: none +X-DNS-Prefetch-Control: off +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +Origin-Agent-Cluster: ?1 +X-Download-Options: noopen +X-XSS-Protection: 0 +``` + +This preset still avoids `Cache-Control` and `Server` but includes the extra headers that Helmet adds for historical/compatibility reasons. + +### STRICT preset + +The `STRICT` preset enables stronger protections and is a better fit for security focused deployments that can tolerate tighter restrictions. It is conceptually similar to: + +```http +Cache-Control: no-store, max-age=0 +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Opener-Policy: same-origin +Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none' +Strict-Transport-Security: max-age=63072000; includeSubDomains +Permissions-Policy: geolocation=(), microphone=(), camera=() +Referrer-Policy: no-referrer +Server: +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +``` + +Start with `BALANCED` and move to `STRICT` once you have validated that your application works correctly with the stricter Content Security Policy, caching, and frame restrictions. `STRICT` no longer sets HSTS preload by default, so you can opt-in separately when you are ready. + --- -## **Policy Builders** +## Policy builders -`secure.py` allows you to customize headers such as **Content-Security-Policy** and **Permissions-Policy** with ease: +`secure` lets you build rich header values through small, focused builder classes. Two common examples are `ContentSecurityPolicy` and `PermissionsPolicy`. -### **Content-Security-Policy Example** +### Content Security Policy ```python -import secure +from secure import Secure +from secure.headers import ContentSecurityPolicy -# Build a custom CSP policy csp = ( - secure.ContentSecurityPolicy() + ContentSecurityPolicy() .default_src("'self'") - .script_src("'self'", "cdn.example.com") + .script_src("'self'", "cdn.typeerror.com") .style_src("'unsafe-inline'") - .img_src("'self'", "images.example.com") - .connect_src("'self'", "api.example.com") + .img_src("'self'", "images.typeerror.com") + .connect_src("'self'", "api.typeerror.com") ) -# Apply it to secure headers -secure_headers = secure.Secure(csp=csp) +secure_headers = Secure(csp=csp) ``` -**Resulting HTTP headers:** +Resulting header: ```http -Content-Security-Policy: default-src 'self'; script-src 'self' cdn.example.com; style-src 'unsafe-inline'; img-src 'self' images.example.com; connect-src 'self' api.example.com +Content-Security-Policy: default-src 'self'; script-src 'self' cdn.typeerror.com; style-src 'unsafe-inline'; img-src 'self' images.typeerror.com; connect-src 'self' api.typeerror.com ``` -### **Permissions-Policy Example** +You can treat the CSP builder as a safe string builder for CSP directives and keep all CSP logic in one place. + +### Permissions Policy ```python -import secure +from secure import Secure +from secure.headers import PermissionsPolicy -# Build a custom Permissions Policy permissions = ( - secure.PermissionsPolicy() - .geolocation("'self'") - .camera("'none'") - .microphone("'none'") + PermissionsPolicy().geolocation("'self'").camera("'none'").microphone("'none'") ) -# Apply it to secure headers -secure_headers = secure.Secure(permissions=permissions) +secure_headers = Secure(permissions=permissions) ``` -**Resulting HTTP headers:** +Resulting header: ```http -Permissions-Policy: geolocation=('self'), camera=('none'), microphone=('none') +Permissions-Policy: geolocation=(self), camera=(), microphone=() ``` +Other headers, such as `StrictTransportSecurity`, `CrossOriginOpenerPolicy`, `CrossOriginEmbedderPolicy`, `ReferrerPolicy`, `Server`, and `XFrameOptions`, also have small builder classes that mirror their directive structure. + --- -## **Framework Examples** +## Advanced usage: header pipeline and validation + +For most applications, it is enough to construct a `Secure` instance and call `set_headers` or `set_headers_async`. If you want stronger guarantees and clearer failure modes, you can run headers through an explicit pipeline. -### **FastAPI** +```python +import logging + +from secure import COMMA_JOIN_OK, DEFAULT_ALLOWED_HEADERS, MULTI_OK, Secure + +logger = logging.getLogger("secure") + +secure_headers = ( + Secure.with_default_headers() + .allowlist_headers( + allowed=DEFAULT_ALLOWED_HEADERS, + allow_extra=["X-My-App-Header"], + on_unexpected="warn", # "raise" (default), "drop", or "warn" + allow_x_prefixed=False, + logger=logger, + ) + .deduplicate_headers( + action="raise", # "raise" (default), "first", "last", or "concat" + comma_join_ok=COMMA_JOIN_OK, + multi_ok=MULTI_OK, + logger=logger, + ) + .validate_and_normalize_headers( + on_invalid="drop", # "drop" (default), "warn", or "raise" + strict=False, + allow_obs_text=False, + logger=logger, + ) +) +``` + +Key ideas: + +- `allowlist_headers` enforces a case insensitive allowlist of header names and decides what to do with unexpected headers. +- `deduplicate_headers` resolves repeated header names so that you end up with clean `name, value` pairs. +- `validate_and_normalize_headers` validates header names and values, then freezes them into a single valued, immutable mapping exposed via the `.headers` property. +- After the pipeline runs through `validate_and_normalize_headers()`, `Secure` uses the normalized `.headers` mapping when `set_headers` or `set_headers_async` apply the headers, ensuring dropped entries never reach the wire and sanitized values replace unsafe input. + +If you need to emit multi valued headers, such as multiple `Set-Cookie` fields, you can bypass the single valued mapping and work with `header_items()` directly: + +```python +for name, value in secure_headers.header_items(): + response.headers.add(name, value) +``` + +This pipeline gives you a repeatable, testable flow for going from high level policy objects to concrete headers on the wire. + +--- + +## Framework examples + +Below are simple examples for a synchronous and an asynchronous framework. See the framework specific guides for more detailed patterns. + +### Shiny for Python + +#### Recommended: ASGI middleware wrapper + +Wraps the Shiny ASGI application and injects headers by intercepting the ASGI `http.response.start` message. + +```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) +``` + +### FastAPI + +#### Recommended: `add_middleware` (ASGI) + +Injects headers by intercepting the ASGI `http.response.start` message. ```python from fastapi import FastAPI +from secure import Secure +from secure.middleware import SecureASGIMiddleware + +app = FastAPI() +secure_headers = Secure.with_default_headers() + + +@app.get("/") +def read_root(): + return {"Hello": "World"} + + +app.add_middleware(SecureASGIMiddleware, secure=secure_headers) +``` + +#### Alternative: route-level hook (@app.middleware("http")) + +Applies headers directly to the response object returned by `call_next`. +```python +from fastapi import FastAPI from secure import Secure app = FastAPI() @@ -246,11 +511,35 @@ def read_root(): return {"Hello": "World"} ``` +### Starlette + +#### Recommended: `add_middleware` (ASGI) + +```python +from secure import Secure +from secure.middleware import SecureASGIMiddleware +from starlette.applications import Starlette +from starlette.responses import JSONResponse + +secure_headers = Secure.with_default_headers() + +app = Starlette() +app.add_middleware(SecureASGIMiddleware, secure=secure_headers) + + +@app.route("/") +async def read_root(request): + return JSONResponse({"hello": "world"}) +``` + ### Flask +#### Recommended: `after_request` hook + +Applies headers directly to the Flask `Response` object. + ```python from flask import Flask, Response - from secure import Secure app = Flask(__name__) @@ -272,53 +561,115 @@ if __name__ == "__main__": app.run() ``` +#### Alternative: WSGI middleware (`app.wsgi_app`) + +Wraps the WSGI application and injects headers by wrapping `start_response`. +Useful for deployment-level / framework-agnostic WSGI setups. + +```python +from flask import Flask +from secure import Secure +from secure.middleware.wsgi import SecureWSGIMiddleware + +app = Flask(__name__) +secure_headers = Secure.with_default_headers() + + +@app.get("/") +def home(): + return {"Hello": "World"} + + +app.wsgi_app = SecureWSGIMiddleware(app.wsgi_app, secure=secure_headers) + +if __name__ == "__main__": + app.run() +``` + +--- + +## Error handling and logging + +`secure` is designed to fail fast and clearly when something is misconfigured, with hooks for logging and diagnostics. + +### Applying headers + +`set_headers` and `set_headers_async` may raise: + +- `HeaderSetError` when the underlying response object refuses a header or an unexpected error occurs while setting one. +- `AttributeError` when the response object implements neither `set_header(name, value)` nor a mutable `headers` mapping. +- `RuntimeError` from `set_headers` if it detects that the only available setter is asynchronous. In that case, use `set_headers_async` instead. + +### Validation helpers + +The pipeline methods may raise `ValueError` when configured to do so: + +- `allowlist_headers` with `on_unexpected="raise"` when encountering an unexpected header name. +- `deduplicate_headers` with `action="raise"` when it cannot safely resolve duplicates. +- `validate_and_normalize_headers` with `on_invalid="raise"` or when it detects invalid or duplicate entries during normalization. + +Passing a `logger` into these methods is recommended in production so you can see which headers were rejected and why, even when you choose `"drop"` or `"warn"` modes instead of raising. + --- -## **Documentation** +## Documentation -For more details, including advanced configurations and integration examples, please visit the **[full documentation](https://github.com/TypeError/secure/tree/main/docs)**. +For additional examples, framework specific helpers, and more detailed guidance, see the documentation in the `docs` directory: + +- Configuration details. +- Framework integration notes. +- Reference for header builder classes. +- Migration notes for the v2.0.0 release and preset/default changes: + +Documentation: --- -## **Attribution** +## Attribution -This library implements security recommendations from trusted sources: +`secure` implements recommendations from widely used security resources: - [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/)) -We have included attribution comments in the source code where appropriate. +Attribution comments are included in the source code where appropriate. --- -## **Resources** +## Resources -- [OWASP - Secure Headers Project](https://owasp.org/www-project-secure-headers/) +- [OWASP Secure Headers Project](https://owasp.org/www-project-secure-headers/) - [Mozilla Web Security Guidelines](https://infosec.mozilla.org/guidelines/web_security) -- [MDN Web Docs: Security Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers#security) -- [web.dev: Security Best Practices](https://web.dev) -- [The World Wide Web Consortium (W3C)](https://www.w3.org) +- [MDN Web Docs: HTTP Headers](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers) +- [web.dev security guidance](https://web.dev) +- [W3C](https://www.w3.org) + +--- + +## License + +This project is licensed under the terms of the [MIT License](https://opensource.org/licenses/MIT). --- -### **License** +## Contributing -This project is licensed under the terms of the **[MIT License](https://opensource.org/licenses/MIT)**. +Issues and pull requests are welcome. If you’d like to discuss an idea, please open a GitHub issue so we can align on the design before implementation. See [CONTRIBUTING](https://github.com/TypeError/secure/blob/main/CONTRIBUTING.md) for details. --- -## **Contributing** +## Code of Conduct -Contributions are welcome! If you'd like to contribute to `secure.py`, please feel free to open an issue or submit a pull request on **[GitHub](https://github.com/TypeError/secure)**. +See [CODE_OF_CONDUCT](https://github.com/TypeError/secure/blob/main/CODE_OF_CONDUCT.md) for our Code of Conduct. --- -## **Changelog** +## Changelog -For a detailed list of changes, please refer to the **[CHANGELOG](https://github.com/TypeError/secure/blob/main/CHANGELOG.md)**. +See [CHANGELOG](https://github.com/TypeError/secure/blob/main/CHANGELOG.md) for a detailed list of changes by release. --- -## **Acknowledgements** +## Acknowledgements -We would like to thank the contributors of MDN Web Docs and OWASP Secure Headers Project for their invaluable resources and guidelines that help make the web a safer place. +Thank you to everyone who contributes ideas, issues, pull requests, and feedback, as well as the maintainers of MDN and OWASP resources that this project builds on. diff --git a/docs/README.md b/docs/README.md index cbabcca..fedfb79 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ Welcome to the documentation for **Secure Headers**, a flexible Python library f - [Supported Frameworks](#supported-frameworks) - [Security Headers](#security-headers) - [Additional Resources](#additional-resources) +- [Migration Notes](./migration.md) - [Contributing](#contributing) --- @@ -18,7 +19,7 @@ Welcome to the documentation for **Secure Headers**, a flexible Python library f To quickly get started using Secure Headers, check out the basic configuration guide in the main README: -- [Quick Start Guide](../README.md#basic-usage) +- [Quick Start Guide](../README.md#quick-start) For installation instructions, see the [Installation section](./installation.md). @@ -71,6 +72,9 @@ Secure Headers supports many critical HTTP security headers. Below is a list of - [Cross-Origin-Opener-Policy](./headers/cross_origin_opener_policy.md) Prevent attackers from accessing your global objects via cross-origin documents. +- [Cross-Origin-Resource-Policy](./headers/cross-origin-resource-policy.md) + Declare which origins can load your resources to prevent unintended data leaks. + - [Custom Headers](./headers/custom_header.md) Define and manage custom HTTP headers for advanced configurations. @@ -92,6 +96,12 @@ Secure Headers supports many critical HTTP security headers. Below is a list of - [X-Frame-Options](./headers/x_frame_options.md) Protect against clickjacking by controlling whether your content can be framed. +- [X-DNS-Prefetch-Control](./headers/dns_prefetch_control.md) + Control DNS prefetching to avoid leaking outbound link information. + +- [X-Permitted-Cross-Domain-Policies](./headers/x-permitted-cross-domain-policies.md) + Limit legacy cross-domain policy files for Flash/Silverlight compatibility. + --- ## 📚 Additional Resources diff --git a/docs/configuration.md b/docs/configuration.md index 24461a8..69bc6ad 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,22 +2,29 @@ ## Overview -This guide provides detailed information on how to configure `secure.py` beyond the default settings. You can customize security headers, override default behavior, and extend the functionality to meet your application’s unique security requirements. +This guide provides detailed information on how to configure `secure` beyond the default settings. You can customize security headers, override default behavior, and extend the functionality to meet your application’s unique security requirements. --- ## Default Headers -By default, `secure.py` applies a set of widely-used security headers that provide a strong baseline of protection. These include: +`Secure.with_default_headers()` uses `Preset.BALANCED`, which configures a consistent, modern baseline. The defaults cover browser isolation, MIME safety, and legacy compatibility guards while keeping the header set lean: -- **Strict-Transport-Security (HSTS)**: Ensures that browsers only connect to your site over HTTPS. -- **X-Frame-Options**: Protects against clickjacking attacks by controlling whether your site can be embedded in an iframe. -- **X-Content-Type-Options**: Prevents browsers from MIME-sniffing a response away from the declared `Content-Type`. -- **Content-Security-Policy (CSP)**: Mitigates Cross-Site Scripting (XSS) and data injection attacks by defining allowed content sources. +- **Cross-Origin-Opener-Policy:** `same-origin` – isolates the browsing context to prevent exploitation of shared global objects. +- **Cross-Origin-Resource-Policy:** `same-origin` – prevents cross-origin resources from being retrieved unless explicitly permitted. +- **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` – a conservative, CSP-first profile with no inline scripts and forced HTTPS upgrades. +- **Strict-Transport-Security (HSTS):** `max-age=31536000; includeSubDomains` – enforces HTTPS for browsers for one year. +- **Permissions-Policy:** `geolocation=(), microphone=(), camera=()` – disables a few sensitive browser features by default. +- **Referrer-Policy:** `strict-origin-when-cross-origin` – balances privacy and analytics by trimming cross-origin referrer data. +- **Server:** empty string – hides the underlying server software. +- **X-Content-Type-Options:** `nosniff` – blocks MIME sniffing attacks. +- **X-Frame-Options:** `SAMEORIGIN` – prevents framing by other origins. + +Balanced intentionally skips `Cache-Control` and the older compatibility headers (`X-Permitted-Cross-Domain-Policies`, `X-DNS-Prefetch-Control`, `Origin-Agent-Cluster`, `X-Download-Options`, `X-XSS-Protection`), but you can add them manually when your deployment still depends on them. ### Applying Default Headers -To quickly apply these default headers, use the following command: +To quickly apply this configuration, use: ```python secure_headers = Secure.with_default_headers() @@ -36,8 +43,10 @@ Each security header can be customized to meet your application’s unique needs If you want to allow your site to be embedded in an iframe, but only by pages from the same origin, use the following configuration: ```python +from secure import Secure, XFrameOptions + secure_headers = Secure( - xfo=Secure.XFrameOptions().sameorigin() + xfo=XFrameOptions().sameorigin() ) ``` @@ -48,8 +57,10 @@ This protects against clickjacking while maintaining functionality for same-orig To ensure that all subdomains of your site are accessed over HTTPS, and to add your domain to the HSTS preload list, you can configure `Strict-Transport-Security` like this: ```python +from secure import Secure, StrictTransportSecurity + secure_headers = Secure( - hsts=Secure.StrictTransportSecurity().max_age(63072000).include_subdomains().preload() + hsts=StrictTransportSecurity().max_age(63072000).include_subdomains().preload() ) ``` @@ -77,23 +88,33 @@ In this example, a custom HTTP header `X-Custom-Header` is added to the response ## Combining Presets with Customization -You can use one of the built-in presets as a starting point and then further customize specific headers to meet your security needs. +You can use one of the built-in presets as a starting point and then further customize specific headers to meet your security needs. Every `Secure` instance exposes its configuration as a list of header builders via `headers_list`, so you can replace, reorder, or extend that list to adjust individual headers even after instantiation. ### Example: Customizing a Preset ```python -from secure import Secure, Preset +from secure import Preset, Secure, StrictTransportSecurity secure_headers = Secure.from_preset(Preset.BASIC) -secure_headers.hsts.max_age(63072000) # Override the default max-age value + +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 approach allows you to quickly set up basic security headers while customizing certain parameters to fit your application’s security posture. +This replaces the preset’s `Strict-Transport-Security` builder with a custom one while keeping the remaining headers unchanged. --- ## Summary -`secure.py` offers flexibility in how you configure your security headers. Whether you’re using the default settings, customizing individual headers, or adding custom headers, the library allows you to secure your application effectively. For more advanced use cases, consider combining presets with custom configurations. +`secure` offers flexibility in how you configure your security headers. Whether you’re using the default settings, customizing individual headers, or adding custom headers, the library allows you to secure your application effectively. For more advanced use cases, consider combining presets with custom configurations. For more details on each supported header, refer to the [Security Headers Documentation](./headers). diff --git a/docs/frameworks.md b/docs/frameworks.md index 040608c..4673848 100644 --- a/docs/frameworks.md +++ b/docs/frameworks.md @@ -1,6 +1,6 @@ # Framework Integration -`secure.py` supports several popular Python web frameworks. Below are examples showing how to set the default security headers in each framework, along with a brief introduction and links to each project. Additionally, we provide guidance for integrating Secure Headers with custom or unsupported frameworks. +`secure` supports several popular Python web frameworks. Below are examples showing how to set the default security headers in each framework, along with a brief introduction and links to each project. Additionally, we provide guidance for integrating Secure Headers with custom or unsupported frameworks. ## Table of Contents @@ -27,7 +27,7 @@ ### Note: Overriding the `Server` Header in Uvicorn-based Frameworks -If you're using Uvicorn as the ASGI server (commonly used with frameworks like FastAPI, Starlette, and others), Uvicorn automatically injects a `Server: uvicorn` header into all HTTP responses by default. This can lead to multiple `Server` headers when using `Secure.py` to set a custom `Server` header. +If you're using Uvicorn as the ASGI server (commonly used with frameworks like FastAPI, Starlette, and others), Uvicorn automatically injects a `Server: uvicorn` header into all HTTP responses by default. This can lead to multiple `Server` headers when using `secure` to set a custom `Server` header. To prevent Uvicorn from adding its default `Server` header, you can disable it by passing the `--no-server-header` option when running Uvicorn, or by setting `server_header=False` in the `uvicorn.run()` method: @@ -149,14 +149,7 @@ class HelloWorld: cherrypy.response.headers.update(secure_headers.headers) return b"Hello, world" -config = { - "/": { - "tools.response_headers.on": True, - "tools.response_headers.headers": secure_headers.headers - } -} - -cherrypy.quickstart(HelloWorld(), "/", config) +cherrypy.quickstart(HelloWorld()) ``` ### Single Route Example @@ -178,6 +171,51 @@ cherrypy.quickstart(HelloWorld()) --- +## Dash + +**[Dash](https://dash.plotly.com/)** is a Python framework for building interactive data apps and dashboards, built on top of Plotly.js, React, and Flask. + +### Middleware Example + +```python +import dash +from dash import html +from secure import Secure + +secure_headers = Secure.with_default_headers() + +app = dash.Dash(__name__) +server = app.server + +app.layout = html.Div("Hello Dash!") + + +@server.after_request +def add_security_headers(response): + secure_headers.set_headers(response) + return response +``` + +#### Alternative: WSGI middleware + +```python +import dash +from dash import html +from secure import Secure +from secure.middleware.wsgi import SecureWSGIMiddleware + +secure_headers = Secure.with_default_headers() + +app = dash.Dash(__name__) +server = app.server # Flask app underneath Dash + +app.layout = html.Div("Hello Dash!") + +server.wsgi_app = SecureWSGIMiddleware(server.wsgi_app, secure=secure_headers) +``` + +--- + ## Django **[Django](https://www.djangoproject.com)** is a high-level Python web framework that encourages rapid development and clean, pragmatic design. @@ -262,7 +300,19 @@ app.add_route("/", HelloWorldResource()) **[FastAPI](https://fastapi.tiangolo.com)** is a modern, fast web framework for building APIs with Python 3.6+. -### Middleware Example +#### 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: route-level hook with `@app.middleware("http")` ```python from fastapi import FastAPI @@ -314,6 +364,19 @@ def add_security_headers(response: Response): return response ``` +#### Alternative: WSGI middleware + +```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) +``` + ### Single Route Example ```python @@ -529,6 +592,7 @@ secure_headers = Secure.with_default_headers() @app.middleware("response") async def add_security_headers(request, resp): secure_headers.set_headers(resp) + return resp ``` ### Single Route Example @@ -549,6 +613,33 @@ async def index(request): --- +## Shiny + +**[Shiny](https://shiny.posit.co/py/)** is a fully reactive framework for building rich, interactive web apps in pure Python—without needing to learn JavaScript or front-end frameworks. + +### Middleware Example + +```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) +``` + +--- + ## Starlette **[Starlette](https://www.starlette.io)** is a lightweight ASGI framework. @@ -556,33 +647,40 @@ async def index(request): ### Middleware Example ```python -from starlette.applications import Starlette -from starlette.responses import Response -from starlette.middleware.base import BaseHTTPMiddleware from secure import Secure +from secure.middleware import SecureASGIMiddleware +from starlette.applications import Starlette +from starlette.responses import JSONResponse secure_headers = Secure.with_default_headers() -class SecurityHeadersMiddleware(BaseHTTPMiddleware): - async def dispatch(self, request, call_next): - response = await call_next(request) - await secure_headers.set_headers_async(response) - return response +app = Starlette() +app.add_middleware(SecureASGIMiddleware, secure=secure_headers) + + +@app.route("/") +async def read_root(request): + return JSONResponse({"hello": "world"}) ``` ### Single Route Example ```python +from secure import Secure from starlette.applications import Starlette from starlette.responses import Response -from secure import Secure +from starlette.routing import Route secure_headers = Secure.with_default_headers() + async def homepage(request): response = Response("Hello, world") await secure_headers.set_headers_async(response) return response + + +app = Starlette(routes=[Route("/", homepage)]) ``` --- @@ -675,7 +773,7 @@ def index(): ## Custom Frameworks -If you are using a framework that is not listed here, `secure.py` can still be integrated. Most frameworks offer a way to manipulate response headers, which is all you need to apply security headers. +If you are using a framework that is not listed here, `secure` can still be integrated. Most frameworks offer a way to manipulate response headers, which is all you need to apply security headers. ### General Steps: @@ -690,7 +788,7 @@ If you are using a framework that is not listed here, `secure.py` can still be i ```python from secure import Secure -secure_headers = Secure().with_default_headers() +secure_headers = Secure.with_default_headers() def add_secure_headers(response): secure_headers.set_headers(response) diff --git a/docs/headers/cache_control.md b/docs/headers/cache_control.md index 951a39c..26ef61b 100644 --- a/docs/headers/cache_control.md +++ b/docs/headers/cache_control.md @@ -1,57 +1,136 @@ -# Cache-Control Header +# Cache-Control ## Purpose -The `Cache-Control` header is used to define caching mechanisms for both requests and responses. Properly configuring this header can ensure sensitive data isn't inadvertently cached, and resources are served fresh when necessary. +`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. -## Best Practices +## Default behavior -- **`no-store`**: Prevents any caching of sensitive data. -- **`max-age=0`**: Ensures the content is always fresh. -- **`immutable`**: Helps improve caching efficiency for resources that never change. -- **`no-transform`**: Ensures the intermediaries do not modify the content. +If you create `CacheControl()` and do not add directives, it returns the library default value: -## Configuration in `secure.py` +- **Default header value:** `no-store, max-age=0` -The `CacheControl` class in `secure.py` allows flexible configuration of this header. It supports adding directives like `no-cache`, `no-store`, `max-age`, and others to ensure appropriate caching behavior. +This is a secure baseline intended to prevent storage of sensitive responses. -### Example Configuration +## Using with `Secure` ```python -secure_headers = Secure( - cache=CacheControl() - .no_store() - .max_age(0) +from secure import CacheControl, Secure + +secure = Secure( + cache=CacheControl().no_store().max_age(0) ) ``` -### Methods Available +If you don’t configure any directives, the default value is emitted. + +## 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) -- **`no_store()`**: Prevents all caching. -- **`no_cache()`**: Requires revalidation before caching. -- **`max_age(seconds)`**: Specifies the maximum time a resource is considered fresh. -- **`must_revalidate()`**: Forces caches to revalidate content before serving it. -- **`immutable()`**: Indicates that the resource is immutable and doesn't need revalidation. +If your assets are fingerprinted (e.g., `/app.4f3c1.js`), you can cache them aggressively: -## Example Usage +```python +cc = CacheControl().public().max_age(31536000).immutable() +print(cc.header_value) # public, max-age=31536000, immutable +``` -To set up a `Cache-Control` header with no caching allowed and a max-age of 0: +### 4) Shared caches (CDNs/proxies) vs browser caches ```python -cache_control = CacheControl().no_cache().no_store().max_age(0) -print(cache_control.header_name) # Output: 'Cache-Control' -print(cache_control.header_value) # Output: 'no-cache, no-store, max-age=0' +cc = CacheControl().s_maxage(604800).max_age(60) +print(cc.header_value) # s-maxage=604800, max-age=60 ``` -This can then be applied as part of your Secure headers configuration. +`s-maxage` applies to shared caches and overrides `max-age` for them. + +### 5) Stale content during revalidation / on error ```python -secure_headers = Secure(cache=cache_control) +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 ``` -## **Attribution** +## 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 from trusted sources: +This library implements security recommendations and behavior described by: -- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) (licensed under [CC-BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/)) +- [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/)) diff --git a/docs/headers/content_security_policy.md b/docs/headers/content_security_policy.md index e5fbabb..4cab849 100644 --- a/docs/headers/content_security_policy.md +++ b/docs/headers/content_security_policy.md @@ -1,166 +1,259 @@ -# Content-Security-Policy Header +# Content-Security-Policy (CSP) ## Purpose -The `Content-Security-Policy` (CSP) header helps mitigate cross-site scripting (XSS), data injection, and other attacks by specifying the sources from which content can be loaded. By restricting the sources for different types of content (scripts, styles, images, etc.), CSP provides an additional layer of security. +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.). -## Best Practices +CSP is expressed as a list of **directives** separated by semicolons: -- **`default-src 'self'`**: Restricts content to the same origin by default. -- **`object-src 'none'`**: Disables plugins such as Flash and Java applets. -- **`script-src 'self'`**: Allows JavaScript execution only from your domain. -- **`upgrade-insecure-requests`**: Ensures that any HTTP URLs are upgraded to HTTPS. +```http +Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none' +``` + +## 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 `HeaderDefaultValue.CONTENT_SECURITY_POLICY`. + +## Best-practice baseline + +A common “safe baseline” CSP includes: -## Configuration in `secure.py` +- `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` -The `ContentSecurityPolicy` class in `secure.py` allows flexible configuration of CSP. You can add various directives for specific content types like `script-src`, `style-src`, `img-src`, etc., to enhance security. +> CSP is powerful but can break applications if rolled out too aggressively. Start with a report-only policy (see below), review violations, then enforce. -### Example Configuration +--- -```python -secure_headers = Secure( - csp=ContentSecurityPolicy() - .default_src("'self'") - .script_src("'self'") - .style_src("'self'") - .object_src("'none'") +## Configuration with `Secure` + +### Minimal example + +```py +from secure import ContentSecurityPolicy, Secure + +csp = ( + ContentSecurityPolicy() + .default_src(ContentSecurityPolicy.keyword("self")) + .object_src(ContentSecurityPolicy.keyword("none")) + .base_uri(ContentSecurityPolicy.keyword("self")) ) + +secure = Secure(csp=csp) ``` -### Methods Available +Then apply headers in your framework integration: -- **`default_src(*sources)`**: Specifies default content sources. -- **`script_src(*sources)`**: Specifies valid JavaScript sources. -- **`style_src(*sources)`**: Specifies valid CSS sources. -- **`object_src(*sources)`**: Specifies valid plugin sources (e.g., ``, ``, ``). -- **`upgrade_insecure_requests()`**: Automatically upgrades HTTP URLs to HTTPS. +```py +# Flask example +from flask import Flask, Response -## Example Usage +app = Flask(__name__) +secure = Secure(csp=csp) -To set up a `Content-Security-Policy` header that only allows content from the same origin and restricts the use of plugins: +@app.after_request +def add_security_headers(response: Response) -> Response: + secure.set_headers(response) + return response +``` + +### Report-only mode + +To observe violations without enforcing (recommended for rollout): -```python -csp = ContentSecurityPolicy() - .default_src("'self'") - .script_src("'self'") - .object_src("'none'") -print(csp.header_name) # Output: 'Content-Security-Policy' -print(csp.header_value) # Output: "default-src 'self'; script-src 'self'; object-src 'none'" +```py +csp_report_only = ( + ContentSecurityPolicy() + .report_only() + .default_src(ContentSecurityPolicy.keyword("self")) + .script_src(ContentSecurityPolicy.keyword("self")) +) + +secure = Secure(csp=csp_report_only) ``` -This can then be applied as part of your Secure headers configuration. +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 -secure_headers = Secure(csp=csp) +```py +ContentSecurityPolicy.keyword("self") # "'self'" +ContentSecurityPolicy.keyword("none") # "'none'" ``` -## Using Nonce and `strict-dynamic` in Content-Security-Policy +### Nonces -Content Security Policy (CSP) allows you to enhance the security of your web application by specifying the allowed sources of content. Using a nonce with the `strict-dynamic` directive improves protection against Cross-Site Scripting (XSS) attacks by dynamically allowing only scripts that are explicitly marked with a nonce. This is especially useful when you want to allow some inline scripts while ensuring only those scripts and their dynamically loaded dependencies are executed. +Use `nonce()` to produce a CSP nonce source expression: -### Example: Using Nonce with `strict-dynamic` +```py +nonce_value = "abc123==" # base64 / url-safe base64 +ContentSecurityPolicy.nonce(nonce_value) # "'nonce-abc123=='" +``` -Here’s how to set a CSP with a nonce and `strict-dynamic` using `secure.py`: +--- -```python -import uuid +## Escape hatches -from flask import Flask, Response +### Set an exact policy string -from secure import ContentSecurityPolicy, Secure +If you need full control (or want to carry over an existing CSP string), use `.value(...)`: -app = Flask(__name__) +```py +csp = ContentSecurityPolicy().value( + "default-src 'self'; script-src 'self' https://cdn.example; object-src 'none'" +) +``` + +`.set(...)` is an alias for `.value(...)`. +### Clear configuration + +```py +csp = ContentSecurityPolicy().default_src(ContentSecurityPolicy.keyword("self")) +csp.clear() # resets back to library default behavior +``` -def generate_nonce(): - # Create a unique nonce for each request - return uuid.uuid4().hex +### Custom directives +If you need a directive not covered by a helper method: -secure_headers = Secure( - csp=ContentSecurityPolicy() - .default_src("'self'") - .script_src(ContentSecurityPolicy().nonce(generate_nonce()), "'strict-dynamic'") - .style_src("'self'") - .object_src("'none'") +```py +csp = ( + ContentSecurityPolicy() + .custom_directive("default-src", ContentSecurityPolicy.keyword("self")) + .custom_directive("script-src", ContentSecurityPolicy.keyword("self")) ) +# `.custom(...)` is an alias +``` -@app.after_request -def add_security_headers(response: Response): - # Apply the security headers with a new nonce for each response - nonce = generate_nonce() - secure_headers.set_headers(response) - # Ensure the nonce is passed in the CSP for inline scripts - response.headers["Content-Security-Policy"] = response.headers[ - "Content-Security-Policy" - ].replace("'nonce-'", f"'nonce-{nonce}'") - return response +--- + +## 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. -@app.route("/") -def home(): - # Example HTML with an inline script using the nonce - nonce = generate_nonce() - html = f""" - - - Secure.py with CSP - - - - Hello, world! - - - """ - return Response(html, content_type="text/html") - - -if __name__ == "__main__": - app.run() +### Framework-agnostic CSP construction + +```py +import secrets +from secure import ContentSecurityPolicy + +nonce = secrets.token_urlsafe(16) + +csp = ( + ContentSecurityPolicy() + .default_src(ContentSecurityPolicy.keyword("self")) + .script_src( + ContentSecurityPolicy.nonce(nonce), + ContentSecurityPolicy.keyword("strict-dynamic"), + ) + .object_src(ContentSecurityPolicy.keyword("none")) +) + +print(csp.header_value) +# default-src 'self'; script-src 'nonce-...' 'strict-dynamic'; object-src 'none' ``` -### Example Output Headers +### Flask pattern (nonce shared via `g`) -This example sets the following HTTP headers on the response: +```py +import secrets +from flask import Flask, Response, g -```http -Content-Security-Policy: default-src 'self'; script-src 'nonce-' 'strict-dynamic'; style-src 'self'; object-src 'none' +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(ContentSecurityPolicy.keyword("self")) + .script_src( + ContentSecurityPolicy.nonce(g.csp_nonce), + ContentSecurityPolicy.keyword("strict-dynamic"), + ) + .style_src(ContentSecurityPolicy.keyword("self")) + .object_src(ContentSecurityPolicy.keyword("none")) + ) + Secure(csp=csp).set_headers(response) + return response ``` +In your HTML rendering, use the same nonce: + ```html - - - Secure.py with CSP - - - - Hello, world! - - + ``` -- **`default-src 'self'`**: Only content from the same origin is allowed by default. -- **`script-src 'nonce-' 'strict-dynamic'`**: Only scripts with the nonce or dynamically loaded by trusted scripts are allowed. -- **`style-src 'self'`**: Only CSS from the same origin is allowed. -- **`object-src 'none'`**: The `` element is disabled for additional security. +--- + +## 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy) -### Why Use `strict-dynamic`? +- MDN: CSP guide -The `strict-dynamic` directive allows the CSP to trust dynamically created scripts as long as they are loaded by scripts with a nonce or from a trusted source. This reduces the need to explicitly list external sources in the CSP, improving security by ensuring only scripts with a nonce or trusted dynamic scripts are executed. + - [https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) -For more details on `Content-Security-Policy` and the `nonce` attribute, refer to the following resources: +- OWASP Secure Headers Project -- [MDN Web Docs: Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) -- [OWASP Secure Headers Project: Content-Security-Policy](https://owasp.org/www-project-secure-headers/#content-security-policy) + - [https://owasp.org/www-project-secure-headers/#content-security-policy](https://owasp.org/www-project-secure-headers/#content-security-policy) -## **Attribution** +## Attribution -This library implements security recommendations from trusted sources: +This library implements security recommendations and reference material from: -- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-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/#content-security-policy) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) +- MDN Web Docs (licensed under CC-BY-SA 2.5) +- OWASP Secure Headers Project (licensed under CC-BY-SA 4.0) diff --git a/docs/headers/cross-origin-resource-policy.md b/docs/headers/cross-origin-resource-policy.md new file mode 100644 index 0000000..363aca9 --- /dev/null +++ b/docs/headers/cross-origin-resource-policy.md @@ -0,0 +1,60 @@ +# Cross-Origin-Resource-Policy (CORP) + +## Purpose + +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. + +## 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(...)`. + +### Example Configuration + +```python +secure_headers = Secure( + corp=CrossOriginResourcePolicy().same_origin() +) +``` + +> Library default: if you do not change it, the library’s default value is `same-origin`. + +### 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` (licensed under CC-BY-SA 2.5) +- OWASP Secure Headers Project: Cross-Origin-Resource-Policy (licensed under CC-BY-SA 4.0) diff --git a/docs/headers/cross_origin_embedder_policy.md b/docs/headers/cross_origin_embedder_policy.md index d9ea1c7..c76f428 100644 --- a/docs/headers/cross_origin_embedder_policy.md +++ b/docs/headers/cross_origin_embedder_policy.md @@ -1,50 +1,90 @@ -# Cross-Origin-Embedder-Policy Header +# Cross-Origin-Embedder-Policy (COEP) ## Purpose -The `Cross-Origin-Embedder-Policy` (COEP) header prevents a document from loading any cross-origin resources that don’t explicitly grant permission. It works alongside the `Cross-Origin-Resource-Policy` (CORP) to enhance security, especially in modern applications dealing with cross-origin requests. +The **`Cross-Origin-Embedder-Policy`** response header configures the current document’s policy for **loading and embedding cross-origin resources**. -## Best Practices +At a high level, COEP lets you: -- **`require-corp`**: Only allow loading resources from the same origin or those explicitly permitting cross-origin access. This is the most secure setting. -- **`unsafe-none`**: Permits loading cross-origin resources without explicit permission. Use this only when security constraints aren't a concern. +- 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`). -## Configuration in `secure.py` +## Directive values -The `CrossOriginEmbedderPolicy` class in `secure.py` allows you to configure the COEP header with options like `require-corp` or `unsafe-none` to control resource loading policies. +COEP is a **single-value** header (choose one): -### Example Configuration +| 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 -secure_headers = Secure( - coep=CrossOriginEmbedderPolicy().require_corp() -) +from secure.headers import CrossOriginEmbedderPolicy + +coep = CrossOriginEmbedderPolicy().unsafe_none() ``` -### Methods Available +## Cross-origin isolation (COOP + COEP) -- **`require_corp()`**: Ensures resources are loaded only from the same origin or from origins that grant explicit permission. -- **`unsafe_none()`**: Disables the COEP policy, allowing cross-origin resources to be loaded without restriction. +Some powerful browser features require your document to be **cross-origin isolated**. To enable this, you generally need: -## Example Usage +- `Cross-Origin-Embedder-Policy: require-corp` **or** `credentialless`, and +- `Cross-Origin-Opener-Policy: same-origin`. -To set up a `Cross-Origin-Embedder-Policy` header that requires cross-origin resources to explicitly permit being loaded: +## Usage with `Secure` ```python -coep = CrossOriginEmbedderPolicy().require_corp() -print(coep.header_name) # Output: 'Cross-Origin-Embedder-Policy' -print(coep.header_value) # Output: 'require-corp' +from secure import Secure +from secure.headers import CrossOriginEmbedderPolicy, CrossOriginOpenerPolicy + +secure = Secure( + coep=CrossOriginEmbedderPolicy().require_corp(), + coop=CrossOriginOpenerPolicy().same_origin(), +) + +# Inspect emitted headers: +print(secure.header_items()) ``` -This can then be applied as part of your Secure headers configuration. +## Header builder API ```python -secure_headers = Secure(coep=coep) +from secure.headers import CrossOriginEmbedderPolicy + +coep = ( + CrossOriginEmbedderPolicy() + .credentialless() # or .require_corp() / .unsafe_none() +) + +print(coep.header_name) # "Cross-Origin-Embedder-Policy" +print(coep.header_value) # "credentialless" ``` -## **Attribution** +### Methods + +- `unsafe_none()` — set value to `unsafe-none` +- `require_corp()` — set value to `require-corp` +- `credentialless()` — set value to `credentialless` +- `set(value)` — escape hatch: set a custom value (string) +- `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 from trusted sources: +This library implements security recommendations and definitions from trusted sources: -- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-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-embedder-policy) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) +- MDN Web Docs (CC-BY-SA 2.5): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cross-Origin-Embedder-Policy +- OWASP Secure Headers Project (CC-BY-SA 4.0): https://owasp.org/www-project-secure-headers/#cross-origin-embedder-policy diff --git a/docs/headers/cross_origin_opener_policy.md b/docs/headers/cross_origin_opener_policy.md index afdadfa..bb2c6d2 100644 --- a/docs/headers/cross_origin_opener_policy.md +++ b/docs/headers/cross_origin_opener_policy.md @@ -1,52 +1,66 @@ -# Cross-Origin-Opener-Policy Header +# Cross-Origin-Opener-Policy ## Purpose -The `Cross-Origin-Opener-Policy` (COOP) header isolates your browsing context from potentially untrusted contexts, preventing attackers from accessing your global object through popups and mitigating cross-origin attacks such as XS-Leaks. By setting this header, you protect against malicious cross-origin interactions that could compromise your application's security. +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**. -## Best Practices +## 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`. -- **`same-origin`**: This is the most secure option, isolating your document from other origins. -- **`same-origin-allow-popups`**: Allows popups while maintaining some level of isolation. -- **`unsafe-none`**: Disables COOP, potentially exposing your application to cross-origin risks. Use this only if isolation is not required. +## Best Practices -## Configuration in `secure.py` +- **`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. -The `CrossOriginOpenerPolicy` class in `secure.py` allows you to configure the COOP header with options like `same-origin`, `same-origin-allow-popups`, or `unsafe-none` to control the behavior of popups and cross-origin contexts. +## Configuration with `secure` -### Example Configuration +Use the `CrossOriginOpenerPolicy` builder and pass it into `Secure(...)`: ```python +from secure import Secure +from secure.headers import CrossOriginOpenerPolicy + secure_headers = Secure( coop=CrossOriginOpenerPolicy().same_origin() ) ``` -### Methods Available +## Methods Available -- **`same_origin()`**: Isolates the browsing context to the same origin, preventing cross-origin documents from being loaded. -- **`same_origin_allow_popups()`**: Retains references to popups or tabs that opt out of isolation. -- **`unsafe_none()`**: Disables the COOP protection, allowing the document to be added to its opener’s browsing context group. +Directive helpers (recommended): -## Example Usage +- `same_origin()` +- `same_origin_allow_popups()` +- `noopener_allow_popups()` +- `unsafe_none()` -To set up a `Cross-Origin-Opener-Policy` header that isolates your application from cross-origin contexts: +Escape hatches: -```python -coop = CrossOriginOpenerPolicy().same_origin() -print(coop.header_name) # Output: 'Cross-Origin-Opener-Policy' -print(coop.header_value) # Output: 'same-origin' -``` +- `value("...")` / `custom("...")`: Set a raw value (rejects CR/LF). +- `set("...")`: Backwards-compatible alias for `value(...)`. +- `clear()`: Reset back to the library default (`same-origin`). -This can then be applied as part of your Secure headers configuration. +## Example Usage ```python +coop = CrossOriginOpenerPolicy().same_origin() +print(coop.header_name) # 'Cross-Origin-Opener-Policy' +print(coop.header_value) # 'same-origin' + secure_headers = Secure(coop=coop) ``` -## **Attribution** +## 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/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/)) +- MDN Web Docs: Cross-Origin-Opener-Policy (licensed under CC-BY-SA 2.5) +- OWASP Secure Headers Project (licensed under CC-BY-SA 4.0) diff --git a/docs/headers/custom_header.md b/docs/headers/custom_header.md index 4d9019a..697a065 100644 --- a/docs/headers/custom_header.md +++ b/docs/headers/custom_header.md @@ -9,9 +9,9 @@ The `CustomHeader` class allows the creation and management of custom HTTP heade - Custom headers should follow the convention of using a prefix like `X-` (e.g., `X-Custom-Header`), although this is no longer a requirement as per the latest RFC. - Be cautious when adding custom headers to avoid potential conflicts or leaking sensitive information. -## Configuration in `secure.py` +## Configuration with `secure` -The `CustomHeader` class in `secure.py` provides flexibility for developers to define and set custom HTTP headers as needed. You can specify both the header name and value and update the value later if necessary. +The `CustomHeader` class in `secure` provides flexibility for developers to define and set custom HTTP headers as needed. You can specify both the header name and value and update the value later if necessary. ### Example Configuration @@ -22,7 +22,7 @@ custom_header = CustomHeader("X-Custom-Header", "CustomValue") ### Methods Available - **`set(value)`**: Updates the value of the custom header. -- **`header_value()`**: Retrieves the current value of the custom header. +- **`header_value`**: Property that retrieves the current value of the custom header. ## Example Usage @@ -41,6 +41,8 @@ print(custom_header.header_value) # Output: 'NewValue' This can then be applied as part of your Secure headers configuration: ```python +from secure import Secure + secure_headers = Secure(custom=[custom_header]) ``` diff --git a/docs/headers/dns_prefetch_control.md b/docs/headers/dns_prefetch_control.md new file mode 100644 index 0000000..3451d14 --- /dev/null +++ b/docs/headers/dns_prefetch_control.md @@ -0,0 +1,119 @@ +# X-DNS-Prefetch-Control + +## Purpose + +`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. + +## 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` + +```python +from secure import Secure, XDnsPrefetchControl + +secure = Secure( + xdfc=XDnsPrefetchControl().off() +) +``` + +If you don’t configure anything, the default value is emitted. + +## 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). +- Header value sanitization (e.g., blocking CR/LF) is enforced by `Secure.validate_and_normalize_headers(...)`. + +## 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/)) diff --git a/docs/headers/permissions_policy.md b/docs/headers/permissions_policy.md index 268fd74..d2359a3 100644 --- a/docs/headers/permissions_policy.md +++ b/docs/headers/permissions_policy.md @@ -1,22 +1,22 @@ -# Permissions-Policy Header +# Permissions-Policy ## Purpose -The `Permissions-Policy` header allows you to enable or disable browser features and APIs for your web applications. This header replaces the deprecated `Feature-Policy` header and provides fine-grained control over which features are allowed in your site, such as geolocation, camera access, or microphone use. +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. -## Best Practices +In this library, `PermissionsPolicy` is a fluent builder for producing a single `Permissions-Policy` header value, suitable for applying via `Secure`. -- Disable unnecessary features to reduce your site's attack surface and protect user privacy. -- Allow features only for trusted sources or specific origins to avoid potential misuse. -- Use a restrictive default policy, then selectively enable features as required. +## Best practices -## Configuration in `secure.py` +- 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. -The `PermissionsPolicy` class in `secure.py` allows you to configure the `Permissions-Policy` header with specific directives for controlling access to browser APIs and features. - -### Example Configuration +## Configuration with `secure` ```python +from secure import PermissionsPolicy, Secure + secure_headers = Secure( permissions=PermissionsPolicy() .geolocation() @@ -25,42 +25,63 @@ secure_headers = Secure( ) ``` -### Methods Available +## Allowlist syntax -- **`add_directive(directive, *allowlist)`**: Add a custom directive with an optional allowlist of origins. -- **`geolocation(*allowlist)`**: Control access to geolocation data. -- **`camera(*allowlist)`**: Control access to the camera. -- **`microphone(*allowlist)`**: Control access to the microphone. -- **`usb(*allowlist)`**: Control access to USB devices. -- More methods are available for other browser features, such as accelerometer, gyroscope, and more. +`PermissionsPolicy` uses MDN-style allowlist syntax for each directive: -## Example Usage +- **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 -To set up a `Permissions-Policy` header that controls access to specific features: +Examples: ```python -permissions_policy = PermissionsPolicy() - .geolocation() - .camera() - .microphone() -print(permissions_policy.header_name) # Output: 'Permissions-Policy' -print(permissions_policy.header_value) # Output: 'geolocation=(), camera=(), microphone=()' +policy = ( + PermissionsPolicy() + .geolocation("*") # geolocation=* + .camera("self", "https://a.example.com") # camera=(self "https://a.example.com") + .microphone() # microphone=() +) +print(policy.header_value) ``` -This can then be applied as part of your Secure headers configuration. +## 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 Secure +from secure.headers import PermissionsPolicy + +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** +## Resources -- [MDN Web Docs: Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) -- [OWASP Secure Headers Project: Permissions-Policy](https://owasp.org/www-project-secure-headers/#permissions-policy) +- MDN Web Docs: Permissions-Policy (Reference) — 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** +## Attribution This library implements security recommendations from trusted sources: -- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-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/#permissions-policy) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) +- MDN Web Docs (licensed under CC-BY-SA 2.5) +- OWASP Secure Headers Project (licensed under CC-BY-SA 4.0) diff --git a/docs/headers/referrer_policy.md b/docs/headers/referrer_policy.md index 0338b60..618abc3 100644 --- a/docs/headers/referrer_policy.md +++ b/docs/headers/referrer_policy.md @@ -1,60 +1,139 @@ -# Referrer-Policy Header +# Referrer-Policy ## Purpose -The `Referrer-Policy` header controls how much referrer information is included with requests made from your website. This header helps protect user privacy by limiting the amount of data exposed via the `Referer` header, especially when navigating between different origins. +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. -## Best Practices +> Note: `Referer` is intentionally misspelled in HTTP. `Referrer-Policy` does **not** share that misspelling. -- **`strict-origin-when-cross-origin`**: This is a secure default, providing the full URL for same-origin requests and only the origin for cross-origin requests. -- **`no-referrer`**: Use this option to completely suppress the `Referer` header in all requests, offering the most privacy. -- **`no-referrer-when-downgrade`**: Prevents sending the referrer when transitioning from HTTPS to HTTP, protecting information from being leaked over unsecured connections. +## Default behavior -## Configuration in `secure.py` +**Default header value:** `strict-origin-when-cross-origin` -The `ReferrerPolicy` class in `secure.py` allows you to configure the `Referrer-Policy` header with various directives depending on your site's security and privacy requirements. +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`. -### Example Configuration +## 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 -secure_headers = Secure( - referrer=ReferrerPolicy().strict_origin_when_cross_origin() +from secure import ReferrerPolicy, Secure + +secure = Secure( + referrer=ReferrerPolicy() # uses the default: strict-origin-when-cross-origin ) ``` -### Methods Available +### Set a single explicit policy + +Use `value(...)` (or `custom(...)`) when you want to **replace** any configured policies and set exactly one value: -- **`no_referrer()`**: No `Referer` header will be sent. -- **`no_referrer_when_downgrade()`**: The `Referer` header will not be sent when navigating from HTTPS to HTTP. -- **`origin()`**: The `Referer` header will contain only the origin of the URL. -- **`same_origin()`**: The `Referer` header will be sent only for same-origin requests. -- **`strict_origin_when_cross_origin()`**: The full URL is sent for same-origin requests, but only the origin for cross-origin requests. -- **`unsafe_url()`**: The full URL will always be sent, even for cross-origin requests. This may expose sensitive information. +```python +from secure import ReferrerPolicy, Secure -## Example Usage +secure = Secure( + referrer=ReferrerPolicy().value("no-referrer") +) +``` -To set up a `Referrer-Policy` header that limits the exposure of referrer information: +You can also use the fluent directive helpers: ```python -referrer_policy = ReferrerPolicy().strict_origin_when_cross_origin() -print(referrer_policy.header_name) # Output: 'Referrer-Policy' -print(referrer_policy.header_value) # Output: 'strict-origin-when-cross-origin' +secure = 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 ``` -This can then be applied as part of your Secure headers configuration: +You can build the same list with `.add(...)`: ```python -secure_headers = Secure(referrer=referrer_policy) +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** +## Resources -- [MDN Web Docs: Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) -- [OWASP Secure Headers Project: Referrer-Policy](https://owasp.org/www-project-secure-headers/#referrer-policy) +- 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** +## Attribution This library implements security recommendations from trusted sources: -- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-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/#referrer-policy) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) +- 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/) diff --git a/docs/headers/server.md b/docs/headers/server.md index 5af4fbf..3e17361 100644 --- a/docs/headers/server.md +++ b/docs/headers/server.md @@ -9,9 +9,9 @@ The `Server` header provides information about the server software handling the - **Set an empty value or custom string**: It's generally advisable to set the `Server` header to an empty value (`""`) or use a non-informative value to avoid revealing specific details about the server software. - **Avoid exposing server information**: Avoid leaving the default server response, which may expose sensitive version information. -## Configuration in `secure.py` +## Configuration with `secure` -The `Server` class in `secure.py` allows you to easily control the `Server` header value, with the default value set to an empty string to enhance security. +The `Server` class in `secure` allows you to easily control the `Server` header value, with the default value set to an empty string to enhance security. ### Example Configuration diff --git a/docs/headers/strict_transport_security.md b/docs/headers/strict_transport_security.md index 3f5f823..e48081e 100644 --- a/docs/headers/strict_transport_security.md +++ b/docs/headers/strict_transport_security.md @@ -1,61 +1,116 @@ -# Strict-Transport-Security (HSTS) Header +# Strict-Transport-Security (HSTS) ## Purpose -The `Strict-Transport-Security` (HSTS) header ensures that the application communicates over HTTPS, preventing man-in-the-middle attacks by instructing the browser to automatically upgrade all HTTP connections to HTTPS. Additionally, this header helps protect your site from downgrade attacks, where an attacker might try to force a user to communicate over an insecure HTTP connection. +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. -## Best Practices +> Important: Browsers **ignore** `Strict-Transport-Security` if it is delivered over **insecure HTTP**. You must send it over HTTPS only. -- **Set a long max-age**: A duration of one year (`31536000` seconds) is recommended to enforce HTTPS for an extended period. -- **Include subdomains**: Use the `includeSubDomains` directive to apply the policy to all subdomains. -- **Use the preload directive**: Opt into the HSTS preload list to ensure that browsers always load your site over HTTPS, even on the first visit. +## Default behavior -## Configuration in `secure.py` +If you do not configure any directives, this library emits the default header value: -The `StrictTransportSecurity` class in `secure.py` allows you to configure the `Strict-Transport-Security` (HSTS) header with options like `max-age`, `includeSubDomains`, and `preload`. +- **Default header value:** `max-age=31536000` (one year) -### Example Configuration +## 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` + +The `StrictTransportSecurity` header module supports fluent, chainable configuration: ```python +from secure import Secure +from secure.headers import StrictTransportSecurity + secure_headers = Secure( hsts=StrictTransportSecurity() .max_age(31536000) .include_subdomains() - .preload() ) ``` -### Methods Available +### Preload configuration + +If you opt into preload, the library ensures preload requirements are satisfied: + +```python +from secure.headers 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. -- **`max_age(seconds)`**: Set the maximum duration (in seconds) for which the browser should enforce the HTTPS-only policy. -- **`include_subdomains()`**: Apply the HSTS policy to all subdomains of the domain. -- **`preload()`**: Opt into the HSTS preload list, ensuring that all requests to your site are made over HTTPS, even on the first visit. +- **`preload()`** + Add `preload`: indicates intent to meet HSTS preload requirements. This library: -## Example Usage + - automatically enables `includeSubDomains` + - enforces `max-age >= 31536000` -To set up a `Strict-Transport-Security` header with a one-year max age, including subdomains and opting into the HSTS preload list: +- **`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 -hsts_header = StrictTransportSecurity().max_age(31536000).include_subdomains().preload() -print(hsts_header.header_name) # Output: 'Strict-Transport-Security' -print(hsts_header.header_value) # Output: 'max-age=31536000; includeSubDomains; preload' +from secure.headers import StrictTransportSecurity + +hsts = StrictTransportSecurity().max_age(31536000) +print(hsts.header_value) # 'max-age=31536000' ``` -This can then be applied as part of your Secure headers configuration: +One-year HSTS including subdomains: ```python -secure_headers = Secure(hsts=hsts_header) +from secure.headers import StrictTransportSecurity + +hsts = StrictTransportSecurity().max_age(31536000).include_subdomains() +print(hsts.header_value) # 'max-age=31536000; includeSubDomains' ``` -## **Resources** +## Resources -- [MDN Web Docs: Strict-Transport-Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) -- [OWASP Secure Headers Project: HSTS](https://owasp.org/www-project-secure-headers/#http-strict-transport-security) -- [HSTS Preload List](https://hstspreload.org/) +- 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** +## Attribution This library implements security recommendations from trusted sources: -- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) (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/#http-strict-transport-security) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) +- 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/) diff --git a/docs/headers/x-permitted-cross-domain-policies.md b/docs/headers/x-permitted-cross-domain-policies.md new file mode 100644 index 0000000..1999c1c --- /dev/null +++ b/docs/headers/x-permitted-cross-domain-policies.md @@ -0,0 +1,130 @@ +# X-Permitted-Cross-Domain-Policies + +## Purpose + +`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). + +## 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` + +```python +from secure import Secure, XPermittedCrossDomainPolicies + +secure = Secure( + xpcdp=XPermittedCrossDomainPolicies().none() +) +``` + +If you don’t configure anything, the default value is emitted. + +## 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/)) diff --git a/docs/headers/x_content_type_options.md b/docs/headers/x_content_type_options.md index 5ade1e6..ef581c3 100644 --- a/docs/headers/x_content_type_options.md +++ b/docs/headers/x_content_type_options.md @@ -1,55 +1,76 @@ -# X-Content-Type-Options Header +# X-Content-Type-Options ## Purpose -The `X-Content-Type-Options` header prevents browsers from MIME-sniffing a response away from the declared `Content-Type`. This helps protect against certain types of attacks, such as cross-site scripting (XSS) and drive-by downloads, where an attacker tries to disguise a file's MIME type in order to trick the browser into executing malicious content. +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. ## Best Practices -- **Set to `nosniff`**: This is the recommended value, as it tells the browser to strictly follow the declared `Content-Type` and not attempt to guess or "sniff" the MIME type. This helps prevent MIME-based attacks. +- **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 in `secure` -## Configuration in `secure.py` +The `XContentTypeOptions` class configures `X-Content-Type-Options`. -The `XContentTypeOptions` class in `secure.py` allows you to easily configure the `X-Content-Type-Options` header. The default value is `nosniff`, which is the recommended setting. +**Default header value:** `nosniff` -### Example Configuration +### Minimal configuration ```python +from secure import Secure, XContentTypeOptions + secure_headers = Secure( - xcto=XContentTypeOptions().nosniff() + xcto=XContentTypeOptions().nosniff(), ) ``` -### Methods Available +### Methods available -- **`nosniff()`**: Sets the `X-Content-Type-Options` header to `nosniff`, which prevents MIME-sniffing by browsers. -- **`set(value)`**: Sets a custom value for the `X-Content-Type-Options` header. -- **`clear()`**: Clears any custom value and reverts the header to its default value (`nosniff`). +- **`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`). -## Example Usage +> Note: `set/value` are escape hatches. If you use `Secure.validate_and_normalize_headers(...)`, that layer is responsible for sanitization and safety checks. -To set up the `X-Content-Type-Options` header and prevent MIME-sniffing: +## Example usage ```python -xcto_header = XContentTypeOptions().nosniff() -print(xcto_header.header_name) # Output: 'X-Content-Type-Options' -print(xcto_header.header_value) # Output: 'nosniff' +from secure import XContentTypeOptions + +xcto = XContentTypeOptions().nosniff() +print(xcto.header_name) # 'X-Content-Type-Options' +print(xcto.header_value) # 'nosniff' ``` -This can then be applied as part of your Secure headers configuration: +Apply via `Secure`: ```python -secure_headers = Secure(xcto=xcto_header) +from secure import Secure, XContentTypeOptions + +secure_headers = Secure(xcto=XContentTypeOptions().nosniff()) ``` -## **Resources** +## Resources -- [MDN Web Docs: 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) +- 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** +## Attribution This library implements security recommendations from trusted sources: -- [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) (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-content-type-options) (licensed under [CC-BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)) +- 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/) diff --git a/docs/headers/x_frame_options.md b/docs/headers/x_frame_options.md index ad50514..544117a 100644 --- a/docs/headers/x_frame_options.md +++ b/docs/headers/x_frame_options.md @@ -1,57 +1,100 @@ -# X-Frame-Options Header +# X-Frame-Options -## Purpose +## What it does -The `X-Frame-Options` header helps protect your site from clickjacking attacks by specifying whether your web pages can be embedded in ``, `