Secure v2.0.1 Release Notes

Release Date: 2026-04-22 // about 2 months ago
  • secure v2.0.1

    0️⃣ secure v2.0.1 introduces a cleaner public API, modern preset defaults, first-class ASGI and WSGI middleware, and safer header handling across supported Python web frameworks.

    0️⃣ Compared with v1.0.1, v2 keeps the core Secure, with_default_headers(), from_preset(), set_headers(), and set_headers_async() APIs, but changes defaults, adds middleware and normalization helpers, and makes Secure.headers stricter and effectively read-only. Notably, neither v1 nor v2 exposes secure. __version__ as a public attribute, so version checks should use package metadata rather than module attributes.

    Highlights

    • 🆕 New preset model with Preset.BALANCED as the recommended default
    • First-class SecureASGIMiddleware and SecureWSGIMiddleware
    • 🆕 New header pipeline helpers for allowlisting, deduplication, and normalization
    • Expanded header coverage, including:

      • Cross-Origin-Resource-Policy
      • X-DNS-Prefetch-Control
      • X-Permitted-Cross-Domain-Policies
    • Clearer migration path for users coming from v1 manual response mutation patterns

    Key improvements

    Middleware

    v2 adds framework-agnostic middleware in secure.middleware:

    • SecureASGIMiddleware(app, *, secure=None, multi_ok=None)
    • SecureWSGIMiddleware(app, *, secure=None, multi_ok=None)

    This gives ASGI and WSGI applications a cleaner integration path than manually mutating each response.

    Header pipeline helpers

    v2 adds new helpers on Secure:

    • allowlist_headers()
    • deduplicate_headers()
    • validate_and_normalize_headers()
    • header_items()

    These make it easier to inspect, normalize, and safely emit headers, especially in applications that may need duplicate-aware handling for multi-valued headers.

    🆕 New public builders and constants

    v2 adds:

    • CrossOriginResourcePolicy
    • XDnsPrefetchControl
    • XPermittedCrossDomainPolicies
    • MULTI_OK
    • COMMA_JOIN_OK
    • 👍 DEFAULT_ALLOWED_HEADERS
    More predictable header state

    v2 tracks headers_list mutations correctly. In v1, once .headers had been read, later mutations to headers_list could leave .headers stale because it was backed by a cached property.

    💥 Breaking changes

    Secure.headers is stricter

    In v1, Secure.headers was a cached dict[str, str], and duplicate header names silently collapsed to the last value.

    In v2:

    • Secure.headers is an immutable mapping
    • duplicate header names, case-insensitive, raise ValueError

    If your application intentionally or accidentally creates duplicate header names, do not rely on .headers as the source of truth. Use header_items() for ordered inspection, or call deduplicate_headers() before applying headers.

    0️⃣ Default headers changed

    0️⃣ Secure.with_default_headers() no longer means the same thing it meant in v1.

    • 0️⃣ In v1, with_default_headers() included Cache-Control: no-store
    • 0️⃣ In v2, with_default_headers() maps to Preset.BALANCED, which does not include that same default cache behavior

    0️⃣ If you relied on the v1 default cache policy, add it explicitly in v2.

    Presets changed materially

    v1 included Preset.BASIC and Preset.STRICT.

    0️⃣ v2 adds Preset.BALANCED, and Secure.with_default_headers() now maps to that new preset.

    This also means Preset.BASIC in v2 is not the old BASIC. Compared with v1 BASIC, v2 BASIC adds or changes:

    • COOP
    • CORP
    • CSP
    • HSTS with includeSubDomains
    • X-DNS-Prefetch-Control
    • X-Permitted-Cross-Domain-Policies
    • Origin-Agent-Cluster
    • X-Download-Options
    • X-XSS-Protection

    0️⃣ and drops v1 BASIC defaults such as:

    • Server
    • Cache-Control

    Preset.STRICT also changed:

    • 🔒 v1 STRICT used Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
    • 🚚 v2 STRICT removes preload by default
    • v2 STRICT changes cache control from no-store to no-store, max-age=0

    💅 If you need v1-style HSTS preload behavior, configure it explicitly in v2.

    FastAPI integration changed in practice

    There is no secure.framework.fastapi helper in either version.

    • v1 had no framework module at all
    • v2 introduces generic ASGI and WSGI middleware instead

    ⬆️ For FastAPI and other ASGI frameworks, the recommended upgrade path is to move from per-response mutation to SecureASGIMiddleware.

    ➕ Added

    • SecureASGIMiddleware
    • SecureWSGIMiddleware
    • allowlist_headers(...)
    • deduplicate_headers(...)
    • validate_and_normalize_headers(...)
    • header_items()
    • CrossOriginResourcePolicy
    • XDnsPrefetchControl
    • XPermittedCrossDomainPolicies
    • MULTI_OK
    • COMMA_JOIN_OK
    • 👍 DEFAULT_ALLOWED_HEADERS

    🔄 Changed

    • 0️⃣ Secure.with_default_headers() now returns the balanced preset
    • header deduplication and normalization are now first-class operations
    • 💅 response integration is more robust across sync and async styles
    • applications with duplicate header names now fail fast instead of silently collapsing values
    • framework guidance now emphasizes middleware-based integration for ASGI and WSGI apps

    Migration guide (v1 → v2)

    0️⃣ 1. Review your defaults

    1. Audit any code that reads Secure.headers

    🚚 3. Move ASGI and WSGI apps to middleware

    FastAPI example

    💅 Before, v1-style manual response mutation
    fromfastapiimportFastAPI,RequestfromsecureimportSecureapp=FastAPI()secure\_headers=Secure.with\_default\_headers()@app.middleware("http")asyncdefadd\_security\_headers(request:Request,call\_next):response=awaitcall\_next(request)awaitsecure\_headers.set\_headers\_async(response)returnresponse
    
    💅 After, v2-style ASGI middleware
    fromfastapiimportFastAPIfromsecureimportSecurefromsecure.middlewareimportSecureASGIMiddlewareapp=FastAPI()secure\_headers=(Secure.with\_default\_headers()
        .deduplicate\_headers(action="last")
        .validate\_and\_normalize\_headers()
    )app.add\_middleware(SecureASGIMiddleware,secure=secure\_headers)
    

    ⬆️ Upgrade in 3 steps

    0️⃣ 1. Replace any assumption that with_default_headers() means the same defaults as v1 ⚡️ 2. Audit code that reads Secure.headers and update duplicate-header handling 🚚 3. For ASGI and WSGI apps, move from per-response header mutation to middleware with a pre-normalized Secure instance

    Summary

    🌐 v2.0.1 keeps the core Secure API intact while making the library safer and easier to integrate in modern Python web applications. The biggest changes are the new preset model, middleware-first integration for ASGI and WSGI frameworks, and stricter handling of duplicate and normalized headers.


Previous changes from v2.0.0.rc1

  • A release-candidate for secure v2.0.0 focused on a cleaner public API, modern presets, first-class ASGI/WSGI middleware , and safer header application/validation across frameworks.

    Highlights

    • New preset model with a recommended default: Preset.BALANCED
    • New ASGI + WSGI middleware for framework-agnostic integration
    • New header pipeline helpers for allowlisting, deduping, and validation/normalization
    • 📄 Expanded header coverage and improved docs, examples, and migration guidance

    💥 Breaking changes

    • 0️⃣ Presets redesigned and defaults changed

      • Added Preset.BALANCED, now the recommended default.
      • Secure.with_default_headers() now equals Secure.from_preset(Preset.BALANCED).
      • Preset.BASIC targets Helmet.js default parity.
      • Preset.STRICT no longer enables HSTS preload by default (opt-in separately).
    • Secure.headers is now strict about duplicates

      • Duplicate header names (case-insensitive) raise ValueError.
      • Use header_items() for multi-valued emission, or resolve duplicates via deduplicate_headers() / validate_and_normalize_headers().

    ➕ Added

    • Middleware

      • SecureASGIMiddleware (intercepts ASGI http.response.start)
      • SecureWSGIMiddleware (wraps WSGI start_response)
      • secure.middleware re-exports both; supports multi_ok for safely appending multi-valued headers (e.g. CSP)
    • Header pipeline helpers on Secure

      • allowlist_headers(...) (raise / drop / warn)
      • deduplicate_headers(...) (raise, first, last, concat) with COMMA_JOIN_OK and MULTI_OK
      • validate_and_normalize_headers(...) (RFC 7230 token validation, CR/LF hardening, optional obs-text, immutable normalized override)
    • Serialization

      • header_items() for ordered (name, value) output without enforcing uniqueness
    • Constants / policies

      • MULTI_OK, COMMA_JOIN_OK, DEFAULT_ALLOWED_HEADERS
      • OnInvalidPolicy, OnUnexpectedPolicy, DeduplicateAction
    • Expanded header coverage

      • Cross-Origin-Resource-Policy
      • X-DNS-Prefetch-Control
      • X-Permitted-Cross-Domain-Policies
    • Project & CI

      • CODE_OF_CONDUCT.md, CONTRIBUTING.md
      • GitHub Actions for multi-version tests + Ruff

    🔄 Changed

    • 📄 Docs/README overhaul

      • Middleware usage + multi_ok semantics
      • Clear preset guidance (BALANCED / BASIC / STRICT) and documented default header set
      • New “header pipeline and validation” section (allowlist → dedupe → normalize)
      • New error handling/logging guidance (HeaderSetError, AttributeError, RuntimeError, pipeline ValueError)
      • Supported frameworks list expanded (now includes Dash and Shiny)
      • Attribution to MDN and the OWASP Secure Headers Project
    • Presets behavior

      • BASIC adds Origin-Agent-Cluster, X-Download-Options, X-XSS-Protection: 0 for Helmet-parity
    • Response integration

      • More robust sync/async detection
      • Supports response.headers.set(...) (Werkzeug-style)
      • Failures while applying headers are wrapped in HeaderSetError
    • Packaging/tooling

      • pyproject.toml modernized (metadata cleanup, setuptools floor bump, Ruff configuration)

    ✅ Testing

    • 🔀 Expanded unit and contract tests, including improved coverage for sync/async response integration paths.

    ⬆️ Upgrade notes

    • 0️⃣ If you were relying on the previous with_default_headers() behavior, review the new presets and choose:

      • Preset.BALANCED (default, recommended)
      • Preset.BASIC (Helmet-parity compatibility)
      • Preset.STRICT (hardened; no preload by default)
    • 🔧 If your app needs multi-valued headers, prefer header_items() and/or configure middleware multi_ok.

    👀 See the migration guide: docs/migration.md.

    What's Changed

    • 👷 feat: CI for unit tests + explicit Python 3.13 & 3.14 support by @BoboTiG in #39
    • ⚡️ secure v2.0.0rc1: presets redesign, ASGI/WSGI middleware, and header updates by @cak in #40

    🆕 New Contributors

    Full Changelog : v1.0.1...v2.0.0rc1