secure
A small, focused library for adding modern security headers to Python web applications.
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:
- 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:
Why use 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.
Supported 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.
Features
-
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.
Requirements
-
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.
Installation
You can install secure with your preferred Python package manager.
Using uv
Using pip
Quick start
The core entry point is the Secure class. A typical simple setup looks like this:
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.
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.
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:
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
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.
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:
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 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
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-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.
# 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:
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:
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:
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
secure lets you build rich header values through small, focused builder classes. Two common examples are ContentSecurityPolicy and PermissionsPolicy.
Content Security Policy
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:
You can treat the CSP builder as a safe string builder for CSP directives and keep all CSP logic in one place.
Permissions Policy
from secure.headers import PermissionsPolicy
permissions = (
PermissionsPolicy().geolocation("'self'").camera("'none'").microphone("'none'")
)
secure_headers = Secure(permissions=permissions)
Resulting header:
Other headers, such as StrictTransportSecurity, CrossOriginOpenerPolicy, CrossOriginEmbedderPolicy, ReferrerPolicy, Server, and XFrameOptions, also have small builder classes that mirror their directive structure.
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.
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:
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.
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.
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.
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"}
Starlette
Recommended: add_middleware (ASGI)
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.
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()
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.
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:
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.
Validation helpers
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.
Documentation
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
Attribution
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.
Resources
- OWASP Secure Headers Project
- Mozilla Web Security Guidelines
- MDN Web Docs: HTTP Headers
- web.dev security guidance
- W3C
License
This project is licensed under the terms of the MIT License.
Contributing
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.
Code of Conduct
See CODE_OF_CONDUCT for our Code of Conduct.
Changelog
See CHANGELOG for a detailed list of changes by release.
Acknowledgements
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.