A small, focused library for adding modern security headers to Python web applications.
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:
- 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:
import secure- 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.
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.
-
Secure headers
Apply headers likeStrict-Transport-Security,Content-Security-Policy,X-Content-Type-Options,X-Frame-Options, and more. -
Presets with secure defaults
Start from opinionated presets likePreset.BASICandPreset.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. -
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.
-
Python 3.10 or higher
securetargets modern Python and is currently tested on Python 3.10 through 3.13.It uses features introduced in Python 3.10, including:
- Union type operator (
|) for cleaner type annotations. - Structural pattern matching (
match). - Improved typing and annotations.
functools.cached_propertyfor efficient lazy computation.
If you need support for Python 3.6 through 3.9, use version
0.3.0of the library. - Union type operator (
-
Dependencies
This library has no external dependencies outside of the Python standard library.
You can install secure with your preferred Python package manager.
uv add securepip install secureThe core entry point is the Secure class. A typical simple setup looks like this:
import secure
secure_headers = secure.Secure.with_default_headers()
# For a synchronous framework
secure_headers.set_headers(response)
# For an asynchronous framework
await secure_headers.set_headers_async(response)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
headersmapping that supports item assignment.
If your framework uses a different contract, see the framework specific guides or use header_items() to apply headers manually.
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).
Wrap any WSGI stack with SecureWSGIMiddleware, and pass a configured Secure instance if you need a custom CSP or additional headers.
from flask import Flask
from secure import Secure
from secure.middleware import SecureWSGIMiddleware
secure_headers = Secure.with_default_headers()
app = Flask(__name__)
app.wsgi_app = SecureWSGIMiddleware(app.wsgi_app, secure=secure_headers)For Django, apply the headers through a middleware class since Django’s middleware pipeline wraps requests and responses rather than the raw WSGI callable:
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 responseRegister the class in your MIDDLEWARE setting to enforce security headers on every response.
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.
from fastapi import FastAPI
from secure import Secure
from secure.middleware import SecureASGIMiddleware
secure_headers = Secure.with_default_headers()
app = FastAPI()
app.add_middleware(SecureASGIMiddleware, secure=secure_headers)If you need to tailor the CSP, build a custom Secure instance before wiring the middleware:
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:
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)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).
When you call Secure.with_default_headers() (or Secure.from_preset(Preset.BALANCED)), secure configures the recommended defaults that balance security and usability:
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: SAMEORIGINThese 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.
If you prefer to think in terms of profiles instead of individual headers, secure provides presets via the Preset enum and Secure.from_preset.
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)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:
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: SAMEORIGINBalanced 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.
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:
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: 0This preset still avoids Cache-Control and Server but includes the extra headers that Helmet adds for historical/compatibility reasons.
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:
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: DENYStart 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.
secure lets you build rich header values through small, focused builder classes. Two common examples are ContentSecurityPolicy and PermissionsPolicy.
from secure import Secure
from secure.headers import ContentSecurityPolicy
csp = (
ContentSecurityPolicy()
.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")
)
secure_headers = Secure(csp=csp)Resulting header:
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.comYou can treat the CSP builder as a safe string builder for CSP directives and keep all CSP logic in one place.
from secure import Secure
from secure.headers import PermissionsPolicy
permissions = (
PermissionsPolicy().geolocation("'self'").camera("'none'").microphone("'none'")
)
secure_headers = Secure(permissions=permissions)Resulting header:
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.
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.
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_headersenforces a case insensitive allowlist of header names and decides what to do with unexpected headers.deduplicate_headersresolves repeated header names so that you end up with cleanname, valuepairs.validate_and_normalize_headersvalidates header names and values, then freezes them into a single valued, immutable mapping exposed via the.headersproperty.- After the pipeline runs through
validate_and_normalize_headers(),Secureuses the normalized.headersmapping whenset_headersorset_headers_asyncapply 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:
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.
Below are simple examples for a synchronous and an asynchronous framework. See the framework specific guides for more detailed patterns.
Wraps the Shiny ASGI application and injects headers by intercepting the ASGI http.response.start message.
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)Injects headers by intercepting the ASGI http.response.start message.
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)Applies headers directly to the response object returned by call_next.
from fastapi import FastAPI
from secure import Secure
app = FastAPI()
secure_headers = Secure.with_default_headers()
@app.middleware("http")
async def add_security_headers(request, call_next):
response = await call_next(request)
await secure_headers.set_headers_async(response)
return response
@app.get("/")
def read_root():
return {"Hello": "World"}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"})Applies headers directly to the Flask Response object.
from flask import Flask, Response
from secure import Secure
app = Flask(__name__)
secure_headers = Secure.with_default_headers()
@app.after_request
def add_security_headers(response: Response):
secure_headers.set_headers(response)
return response
@app.route("/")
def home():
return "Hello, world"
if __name__ == "__main__":
app.run()Wraps the WSGI application and injects headers by wrapping start_response.
Useful for deployment-level / framework-agnostic WSGI setups.
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()secure is designed to fail fast and clearly when something is misconfigured, with hooks for logging and diagnostics.
set_headers and set_headers_async may raise:
HeaderSetErrorwhen the underlying response object refuses a header or an unexpected error occurs while setting one.AttributeErrorwhen the response object implements neitherset_header(name, value)nor a mutableheadersmapping.RuntimeErrorfromset_headersif it detects that the only available setter is asynchronous. In that case, useset_headers_asyncinstead.
The pipeline methods may raise ValueError when configured to do so:
allowlist_headerswithon_unexpected="raise"when encountering an unexpected header name.deduplicate_headerswithaction="raise"when it cannot safely resolve duplicates.validate_and_normalize_headerswithon_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.
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: https://github.com/TypeError/secure/tree/main/docs/migration.md
Documentation: https://github.com/TypeError/secure/tree/main/docs
secure implements recommendations from widely used security resources:
- MDN Web Docs (licensed under CC-BY-SA 2.5)
- OWASP Secure Headers Project (licensed under CC-BY-SA 4.0)
Attribution comments are included in the source code where appropriate.
- OWASP Secure Headers Project
- Mozilla Web Security Guidelines
- MDN Web Docs: HTTP Headers
- web.dev security guidance
- W3C
This project is licensed under the terms of the MIT License.
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 for details.
See CODE_OF_CONDUCT for our Code of Conduct.
See CHANGELOG for a detailed list of changes by release.
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.