Library / SDK
TypeError/secure avatar
TypeError/secure

secure: one policy object for HTTP security headers across Python frameworks

Modern Python library for HTTP security headers with safe defaults, configurable presets, and first-class ASGI/WSGI middleware (FastAPI, Django, Flask, Shiny, and more).

1,073 stars35 forksPythonMIT

At a glance

What is it?
secure is a dependency-free Python library that configures HTTP security headers once and applies them through ASGI middleware, WSGI middleware or framework response hooks, with BALANCED, BASIC and STRICT presets and builders for the complex headers. Its own documentation is careful to say the defaults are a starting point rather than a substitute for reviewing your own application's policy.
Who is it for?
Adopt secure if your Python application sets security headers by hand in several places and you want one reviewed policy applied consistently, particularly across more than one framework or deployment target. Do not adopt it expecting the defaults to be correct for your application, because the library says plainly that the Content Security Policy in particular has to be adjusted for the scripts, styles, assets and third-party services you actually use.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 52 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem: the same header string, copied into six places

The tagline is Define HTTP security headers once. Apply them consistently across Python web apps, and the library describes itself as providing a small, dependency-free API for configuring modern security headers across common Python web frameworks through ASGI middleware, WSGI middleware, or framework response hooks.

The problem it names is concrete. Setting headers manually is fine for one endpoint, and it gets harder to review when values are copied across routes, response hooks, reverse-proxy settings, and different framework integrations. The stated use case is when you want to avoid copy-pasted header strings spread across handlers, hooks, and middleware.

That is a real problem, but it is worth being honest about its scale. A security header set is about a dozen lines of text. The cost of a project is not typing those lines, it is that you have to know they exist, that you have to keep them in step with each other, and that you have to review them at all. A library earns its place when the policy is complex enough to have a structure, which for Content Security Policy it genuinely is, and when it has to be applied at more than one layer, which in a project with an ASGI app, a WSGI sub-service and a reverse proxy in front it is.

The package is named secure, published on PyPI as secure, and the homepage is typeerror.com/secure. The primary language is Python, the licence is MIT, and the requirements are Python 3.10+ with no external dependencies. That last point is the strongest single fact in the pyproject.toml, because dependencies = [] means the security library you add to a project adds nothing to its dependency graph, and the classifiers include Typing :: Typed, so the package ships type information.

The maintenance picture is good. The repository is not archived, the last push was on 2026-09-01, and the releases show v2.0.0rc1 in December 2025, v2.0.1 in April 2026 with the title Middleware, Presets, and Safer Defaults, and v1.0.1 in October 2024 titled Performance Improvements for Secure.set_headers.

The default header set, and why the CSP is the hard part

The default headers are listed in full in the README, and reading them is the fastest way to understand the library's stance.

http
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Content-Security-Policy: default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests
Strict-Transport-Security: max-age=31536000; includeSubDomains
Permissions-Policy: geolocation=(), microphone=(), camera=()
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

The README says this preset reflects modern browser guidance from MDN and OWASP, that it reduces cross-origin risk, prevents MIME sniffing, and provides a conservative Content Security Policy you can extend.

And then it says the thing that makes this a serious project rather than a header generator. The defaults are a reasonable starting point, not a substitute for application-specific review. Content Security Policy in particular should be adjusted for the scripts, styles, assets, and third-party services your app actually uses. And separately, the default CSP avoids unsafe script execution by default, but CSP is context-sensitive: interactive apps, dashboards, CDNs, analytics, embedded content, or other third-party integrations may require additional configuration.

That is the honest framing, and it is worth reading as a design position rather than a disclaimer. The library will not tell you that a Content Security Policy is a solved problem, because it is not. A CSP that blocks your analytics script is not more secure in any meaningful sense, it is just broken. So the library ships a conservative policy, tells you it is conservative, and gives you the tools to widen it.

One detail in the default CSP is worth a second look for anyone who will extend it. style-src includes 'unsafe-inline', and script-src-attr is 'none'. The first is a concession to how much CSS in the wild is inline, and the second is a hard denial of inline event handlers. That combination is a reasonable starting balance, and it is also a useful illustration that the preset is a compromise rather than a position.

Presets, and the difference between BALANCED and STRICT

There are three presets, and the API for choosing one is a single call:

python
from secure import Preset, Secure

Secure.from_preset(Preset.BALANCED)
Secure.from_preset(Preset.BASIC)
Secure.from_preset(Preset.STRICT)

The descriptions are short. BALANCED is the practical default for many applications, BASIC is Helmet-style compatibility, and STRICT is a tighter CSP and isolation. The guidance is to start with BALANCED, review the generated headers, and move stricter only when needed.

The BASIC description is the one to look at closely, because it names a specific external project. BASIC is the preset named for Helmet-style compatibility, which makes it the one to compare against if you are moving from a stack that already set security headers that way. The name of the dependency in the topic list is not present, but the Helmet reference in the preset description is, and it is a useful anchor for anyone comparing the two ecosystems.

The three-level structure is the right shape for this kind of library, and the reason is that a single correct policy does not exist. A static file server, a JSON API and an interactive dashboard want different things from a Content Security Policy, and a library that forced one opinion would be wrong for two of the three. The BALANCED default in Secure.with_default_headers() maps to Preset.BALANCED, and the README is clear that it is a practical default designed to be customized.

What is missing from the visible documentation is the exact header difference between the three presets, beyond the one-line descriptions. The full details are in the documentation directory, and for an adopter the practical step is to instantiate each preset and print the resulting headers, which is exactly what the review-first instruction implies.

Three ways to apply the policy, and the one that needs thought

The library offers three application paths, and the README frames the choice by scope: use middleware when you can attach secure once and cover the whole application, and use the same policy object in framework hooks, middleware callbacks, or handlers that expose a response object with headers support.

The ASGI middleware attaches once, in the FastAPI style:

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)

The WSGI middleware wraps the application directly:

python
from secure import Secure
from secure.middleware import SecureWSGIMiddleware

secure_headers = Secure.with_default_headers()
app = SecureWSGIMiddleware(app, secure=secure_headers)

And the response hook path is the same object applied by hand:

python
secure_headers.set_headers(response)
# or
await secure_headers.set_headers_async(response)

The middleware section lists three behavioural details that matter. It overwrites headers by default, it allows controlled duplication via multi_ok, and it only modifies HTTP responses in ASGI apps. That last one is a real constraint: in an ASGI application there are also websocket responses and lifespan events, and those are left alone, which is what you want but is worth knowing.

The framework integrations are given for FastAPI, Flask, Django and Shiny for Python, and the README points at a full framework integration guide for additional frameworks, with a note about first-class middleware versus minimal fallback examples. That note is the useful signal, because it tells you that the Django example is a fallback rather than a first-class integration.

The Django example is the minimal version, a class with get_response and a call method that sets the headers on the response. The Flask example is an after_request hook. The Shiny example wraps the App object in the ASGI middleware, and the README adds a note that interactive apps often need CSP configuration for their script, style, and asset loading patterns.

Builders and the validation pipeline

The two complex headers get their own builders, and the API is a chain of calls rather than a string you assemble.

python
from secure.headers import ContentSecurityPolicy

csp = (
    ContentSecurityPolicy()
    .default_src("'self'")
    .script_src("'self'", "cdn.example.com")
)

and the same shape for Permissions Policy:

python
from secure.headers import PermissionsPolicy

permissions = (
    PermissionsPolicy()
    .geolocation("'self'")
    .camera("'none'")
)

The benefit over a string is not the formatting, it is that the builder knows the header's grammar. A Permissions Policy is a list of feature names with allowlists, and a Content Security Policy is a list of directives with source lists, and both have rules about what is valid in a value. A builder can enforce or normalise that, where a hand-written string cannot.

The optional pipeline is the third piece, and it is the part that has the most operational relevance:

python
secure_headers = (
    Secure.with_default_headers()
    .allowlist_headers(...)
    .deduplicate_headers(...)
    .validate_and_normalize_headers(...)
)

The stated effect is that this makes header allowlists, deduplication, normalization, and validation explicit instead of leaving those choices spread across application code.

Each of those four addresses a real failure mode. Deduplication matters when a reverse proxy already sets a header and the application sets it again, because the result is two values of the same header and the browser's behaviour depends on which one it picks. An allowlist matters when you want to guarantee that nothing outside your policy adds a header, which is the case when the same origin is also served by another service. Validation matters because a malformed CSP directive is dropped by the browser, so a typo silently disables that protection. Normalisation matters because the same policy expressed two different ways is two different policies.

The v1.0.1 release title, Performance Improvements for Secure.set_headers, points at the other end of the design: the per-response path is on the hot path of every request, and the library has spent effort making that call cheap.

Where secure is the wrong choice

There are three limits, and the first is about a mismatch that the library itself flags.

The first is that the default preset is often wrong for a real application, and the library says so. A dashboard that loads a charting library from a CDN, an application with analytics, or anything with embedded third-party content will find the default script-src of 'self' blocking the resources it needs, and the first symptom is a broken page rather than a security warning. The library gives you the tools to fix that, but it does not know your dependency list, and the work of building the correct policy is on you.

The second is that the library cannot see the layers around it. A reverse proxy or an ingress controller in front of your application may set its own security headers, and the two sets then interact in ways that are hard to predict. The deduplication and allowlist pipeline exists precisely because of that, and it is opt-in, which means the default configuration is the one most likely to produce duplicate headers in a proxied deployment.

The third is the scope. This is a library for setting HTTP response headers. It is not a Content Security Policy generator that understands your routes, not a cookie configuration helper, not a TLS configuration tool, and it does not set the headers a browser sends. HSTS is set as a response header, which is the right layer, but the server's TLS configuration, the cookie flags, and the permissions of the identity provider are all outside its reach, and the topic list names the headers rather than the whole security surface.

For contrast, there are two other approaches. The first is the middleware model, and secure is one of several libraries that do exactly this, the difference being which frameworks are first-class, which is the whole reason this one has a FastAPI, Django, Flask and Shiny set. The second approach is to set the headers at the edge, in nginx or in a CDN configuration, which has the advantage of applying to every response including the error pages and the static files that never pass through your Python code, and the disadvantage of being outside your application's review and test cycle. The realistic answer for most teams is both, with the edge as the enforcing layer and a library as the place where the policy is written, and the risk being that the two disagree, which is what the deduplication step is for.

Editorial conclusion

Adopt secure if your Python application sets security headers by hand in several places and you want one reviewed policy applied consistently, particularly across more than one framework or deployment target. Do not adopt it expecting the defaults to be correct for your application, because the library says plainly that the Content Security Policy in particular has to be adjusted for the scripts, styles, assets and third-party services you actually use. Verify first that the preset you start from matches the way your application loads assets, that your reverse proxy is not also setting the same headers, and that the validation and deduplication pipeline is enabled if you are running behind something that adds headers of its own.

Frequently asked questions

What does the secure library do for Python web applications?

It provides a small, dependency-free API for configuring modern HTTP security headers across common Python web frameworks, through ASGI middleware, WSGI middleware or framework response hooks, so that one policy object replaces scattered header strings.

Which security headers does secure apply by default?

The BALANCED preset, which Secure.with_default_headers() maps to, applies Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy, a Content Security Policy, Strict-Transport-Security, Permissions-Policy, Referrer-Policy, X-Content-Type-Options and X-Frame-Options, following guidance from MDN and OWASP.

What presets does secure offer?

BALANCED, described as a practical default for many applications, BASIC, described as Helmet-style compatibility, and STRICT, which gives a tighter CSP and isolation. The README advises starting with BALANCED, reviewing the generated headers, and moving stricter only when needed.

Does secure have any dependencies?

No. The pyproject.toml lists an empty dependencies array, requires Python 3.10 or newer, and ships a py.typed marker, so the package adds nothing to a project's dependency graph and carries its own type information.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. TypeError/secure on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/typeerror-secure.svg)](https://hysenlabs.com/projects/typeerror-secure)

Was this useful?