---
key: "framework-security-patterns"
title: "Preserve framework security controls across the request lifecycle"
url: "https://softwaresecurity.com/domains/appsec/secure-coding/framework-security-patterns/"
api: "https://softwaresecurity.com/api/v1/articles/framework-security-patterns.json"
domain: "appsec"
topic: "Application security"
type: "Guide"
identifiers: ["CWE-1188","CWE-288","CWE-489","CWE-352","A02:2025","A01:2025","V13.4.2","V3.3.1","V3.4.1","V3.4.3","V15.3.4"]
tags: ["Secure defaults and frameworks","Java","Python"]
stages: ["build"]
roles: ["developer","appsec-engineer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Preserve framework security controls across the request lifecycle

Trace middleware, persistence and rendering so custom code does not bypass the protections supplied by the framework.

Canonical URL: https://softwaresecurity.com/domains/appsec/secure-coding/framework-security-patterns/

## In short

Framework protections cover only the paths that use them. Keep authentication, CSRF protection and authorization in the shared pipeline for every route, including new APIs, error handlers and jobs. Turn on the production settings that checks such as Django's `check --deploy` verify: debug off, HSTS, secure cookies, a Content Security Policy. Trust forwarded headers only from your own proxy, and review each escape hatch, such as CSRF exemptions, raw SQL or HTML and automatic binding, as a security change.

- Defaults protect only the routes, methods and dispatches the framework pipeline actually covers.
- Never authorize only in middleware or the router: CVE-2025-29927 let one request header skip Next.js middleware entirely.
- Exempt a route from CSRF only when it stops accepting ambient cookies and authenticates another way, such as a webhook signature.
- Trust `X-Forwarded-*` headers only from your own proxy; otherwise clients choose the scheme, host and address you see.
- Raw SQL, raw HTML, automatic binding and debug settings are escape hatches that need review.
- Re-run a request-lifecycle suite when upgrading the framework, the router or the proxy.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| Django | `DEBUG = False`, `ALLOWED_HOSTS`, `SECURE_HSTS_SECONDS = 31536000`, secure session and CSRF cookies, `SECURE_CSP` | `SECURE_CSP` and `ContentSecurityPolicyMiddleware` need Django 6.0 or later. | Django deployment checklist; Django 6.0 release notes |
| Spring Boot and Spring Security | One `SecurityFilterChain` ending in `anyRequest().authenticated()`; `server.forward-headers-strategy=native` only behind a trusted proxy | Spring Boot 4 renamed `server.error.*` to `spring.web.error.*`. | Spring Security reference; Spring Boot application properties |
| ASP.NET Core | An authorization `FallbackPolicy` that requires an authenticated user; `UseHsts`, `UseHttpsRedirection` and `UseAntiforgery` | — | ASP.NET Core security documentation |
| Express | `NODE_ENV=production`, `app.set('trust proxy', 1)` for exactly one proxy, helmet for headers, body size limits | Express has no built-in CSRF protection. | Express production security best practices |
| Ruby on Rails | `config.force_ssl = true`, `config.hosts`, a nonce-based `content_security_policy`, `protect_from_forgery` | — | Rails security guide |
| Next.js | 12.3.5, 13.5.9, 14.2.25, 15.2.3 or later, with authorization repeated in route handlers and data access | — | GitHub advisory GHSA-f82v-jwr5-mffw (CVE-2025-29927) |

## Verify it

1. Run `python manage.py check --deploy --fail-level WARNING` or your framework's equivalent in CI and fix every finding.
2. Send an internal-only header such as `x-middleware-subrequest` from outside and confirm protected routes still deny.
3. Drop the CSRF token while keeping the session cookie on every cookie-authenticated route, including API prefixes.
4. Request through the production proxy with a forged `Host` and `X-Forwarded-Proto` and confirm a 400 or the proxy's own values.
5. Trigger an error with production configuration and confirm no debug page, stack trace or version banner appears.

## What you will learn

- Trace framework controls through actual request paths.
- Review raw APIs and exemptions as explicit boundary changes.
- Build regressions that isolate each lifecycle assumption.

Audience: Developers and tech leads who configure web frameworks and deployments, and reviewers checking that new routes keep the framework's protections.

Before you start:

- Trace middleware ordering, identity establishment, authorization and response serialization.

Time: About 28 minutes: 9 to read and 19 for the exercise

## Follow the request lifecycle instead of assuming defaults apply

Frameworks provide useful security mechanisms, but they protect only the paths that use them correctly. An authenticated page may use escaped templates, CSRF middleware and an ORM, while a newly added JSON endpoint bypasses one of those layers. A setting named `secure` is not an end-to-end property. Trace a request from proxy and router through parsing, session handling, request-intent protection, authorization, database access, serialization and response headers. Identify the exact framework, security modules and deployment mode before copying configuration from another project: the concepts transfer, but configuration names and extension points do not.

The example is a cookie-authenticated profile rename. The intended sequence is authenticate Alice, validate a session-bound CSRF token, authorize Alice's profile, validate the allowed name field, bind the database update and render the new name as text. Each step has a separate purpose. The ORM prevents SQL syntax injection in ordinary parameterized queries; it does not prove Alice owns the selected row. Template escaping protects an output context; it does not validate a URL or authorize a state change.

Middleware-only authorization fails when a request can skip the middleware. In March 2025, CVE-2025-29927 (CVSS 9.1) showed that a request carrying the internal `x-middleware-subrequest` header could bypass Next.js middleware altogether, so applications whose only authorization check ran there exposed protected pages until they upgraded to 12.3.5, 13.5.9, 14.2.25 or 15.2.3. Keep a check where the data is read, so one skipped layer does not expose it.

## Preserve middleware behavior across alternate routes

A common regression adds an API route with a CSRF exemption because the frontend's first request fails. If the route still accepts a session cookie, the original forgery model remains; the correct repair is to send the framework token through its documented integration, as /domains/appsec/browser-security/csrf-origin-defense/ describes. Make deliberate exceptions only for webhooks or machine clients that use their own authentication contract, such as a provider signature, and never disable a browser control for a whole prefix to accommodate one integration. Spring Security and Django have specific request and token lifecycle behavior, so use the documentation for the deployed version instead of transferring a code fragment between frameworks.

Authentication must precede business authorization, and rejected requests must stop before mutation. Parsing limits should apply before oversized bodies consume excessive work. Method overrides, subrouters and exception handlers can change which middleware executes. Behind a trusted proxy, configure external scheme and host handling so secure-cookie and origin decisions use validated infrastructure metadata. Arbitrary forwarded headers must not redefine the application's origin or turn a plain request into a trusted secure request. Check host validation, secure cookie settings, debug mode and production error responses through the actual reverse proxy.

The settings below are the production security block for a Django 6 project behind a TLS-terminating proxy. They turn off debug output, restrict hosts, redirect to HTTPS, send HSTS for a year, mark the session and CSRF cookies `Secure`, and use Django 6.0's built-in Content Security Policy with a per-response nonce and `'strict-dynamic'`. `SECURE_PROXY_SSL_HEADER` is safe only when the proxy sets `X-Forwarded-Proto` itself and removes any value a client sent. In a test project, `manage.py check --deploy --fail-level WARNING` reported no issues on Django 6.0.9 and 6.1.1, a plain-HTTP request was redirected, an unknown `Host` received 400, and the page's script nonce matched the one in the `Content-Security-Policy` header.

```python
# settings_production.py (Django 6.0+): security settings that `manage.py check --deploy` expects.
import os

from django.utils.csp import CSP

DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]  # 50+ random characters, never committed
ALLOWED_HOSTS = ["app.example.test"]

# HTTPS behind a proxy that sets X-Forwarded-Proto and strips it from client requests.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31_536_000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True  # only after submitting the domain to the preload list
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["https://app.example.test"]

# Content Security Policy, built in since Django 6.0; templates use nonce="{{ csp_nonce }}".
SECURE_CSP = {
    "default-src": [CSP.SELF],
    "script-src": [CSP.NONCE, CSP.STRICT_DYNAMIC],
    "object-src": [CSP.NONE],
    "base-uri": [CSP.NONE],
    "form-action": [CSP.SELF],
    "frame-ancestors": [CSP.NONE],
}

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.middleware.csp.ContentSecurityPolicyMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

```

## Turn on the production settings in your framework

Each framework has a short list of settings that decide whether its protections apply in production. Check them in the deployed configuration, because an environment variable can re-enable debug output or remove a middleware class that the source tree looks correct about. Each stack page under /fix/, such as /fix/stack/python-django/ or /fix/stack/java-spring/, lists tested fixes and the baseline versions they assume.

| Framework | Production settings | Common bypass to look for |
| --- | --- | --- |
| Django | `DEBUG = False`, `ALLOWED_HOSTS`, the `SECURE_*` settings above, `SECURE_CSP` (6.0+); `manage.py check --deploy` | `@csrf_exempt` on cookie-authenticated views, `\|safe` and `mark_safe`, `raw()` with string formatting |
| Spring Boot and Spring Security | One `SecurityFilterChain` that ends in `anyRequest().authenticated()`; CSRF left on for browser sessions; `server.forward-headers-strategy` only behind a trusted proxy | `requestMatchers(...).permitAll()` broader than intended, `csrf.ignoringRequestMatchers` on cookie routes, binding requests to entities |
| ASP.NET Core | A `FallbackPolicy` requiring an authenticated user, `UseHsts`, `UseHttpsRedirection`, `UseAntiforgery`, forwarded headers only from known proxies | `[AllowAnonymous]` on controllers, `[IgnoreAntiforgeryToken]`, `Html.Raw` |
| Express | `NODE_ENV=production`, helmet, `express.json({limit})`, `app.set('trust proxy', 1)` for one proxy | No CSRF protection by default, `trust proxy` set to `true` behind an unknown chain, auth middleware that catches its own error and calls `next()` |
| Ruby on Rails | `config.force_ssl`, `config.hosts`, `content_security_policy` with a nonce generator, `protect_from_forgery` | `skip_forgery_protection`, `permit!`, `raw` and `html_safe` |
| Next.js | A patched release; authorization in route handlers, server actions and data access, not only in middleware | Middleware-only checks, as in CVE-2025-29927 |

## Review escape hatches at their actual sinks

An ORM's raw-SQL API may safely accept separate parameters, but concatenating a sort expression or filter into its source text still grants SQL syntax. A template's raw-HTML helper intentionally disables normal escaping and needs a reviewed sanitizer policy if its input is untrusted. Automatic form or entity binding may make a newly added `role` field writable unless the operation explicitly selects fields; the mass assignment fix cards at /fix/mass-assignment/ show explicit field selection per framework. These interfaces are useful precisely because they permit behavior outside the default safety boundary.

Review defaults in the actual configuration rather than relying on a framework's marketing or a prior project. Debug mode, allowed hosts, cookie attributes, session storage and dependency patch level all influence deployed behavior. A reverse proxy may generate an error document without the application's headers, or a cache may serve an authenticated response to another user. Keep explicit projections and authorization at the resource path so framework conveniences cannot silently widen the operation when the data model changes. Review background tasks separately: a queued job does not pass through the request middleware that authorized the interactive request, so it must check membership and tenant again when it runs.

- CSRF exemptions on routes that still accept session cookies.
- Raw SQL built by concatenation, even through an ORM's raw-query API.
- Raw HTML helpers such as `|safe`, `th:utext`, `Html.Raw` and `html_safe` on untrusted values.
- Automatic binding of requests to entities, `fields = '__all__'` and `permit!`.
- Custom exception handlers that log a denial and continue.
- Debug flags, admin consoles and actuator or metrics endpoints left reachable in production.

## Verify one complete request and then break one assumption at a time

Start with a normal rename that persists exactly one allowed field and renders punctuation correctly. Remove the CSRF token while preserving the cookie, change the profile id while preserving the valid token, supply an extra `role` field and submit markup as the name. Each variation isolates a different boundary. Check stored values and rendered structure, not just whether the response is successful.

For operational diagnosis, record which layer rejected the request through an internal category and request id. Repeated token failures after a release may reflect middleware ordering or frontend integration; ownership denials may reflect an incorrect route id. Resist resolving both by disabling guards. Maintain a small request-lifecycle suite for representative routes and repeat it when upgrading the framework, changing routers, adding a proxy or replacing serializers. Framework defaults reduce repetitive work when the application's boundaries stay aligned with them.

## Treat upgrades as changes to a verified contract

A framework upgrade can change defaults, middleware behavior, serializer handling or the supported security API. Preserve your application's behavioral contract across that change instead of merely adapting code until it starts. The rename suite provides concrete probes: missing token still denies, foreign id still denies, unknown role still rejects atomically and markup-shaped names remain text. If a test changes, explain whether the framework behavior changed or the application's policy intentionally changed.

Dependencies around the framework matter too. A JSON body parser may accept shapes the handler did not expect; a session backend outage may affect authentication; a proxy may alter origin metadata; a template plugin may reinterpret strings. Trace each integration at its own interface and include failure behavior. A fallback authentication path should never turn a session-store outage into an anonymous user with default administrative permission. Pin the supported framework version range, track its security releases and retire unsupported branches with an explicit migration plan.

Deployment validation should inspect effective configuration. An environment variable can enable debug output or disable a middleware class despite reviewed source settings. Test the packaged application through the actual proxy and routing arrangement using synthetic data. A local unit test of a view function is valuable, but does not demonstrate that deployed middleware runs in the same order or that the error response avoids a debug page. Keep those verification scopes distinct in the release record so defaults remain useful, observable guarantees rather than assumptions.

## Guided practice

Audit a supplied cookie-backed rename flow with complete policies.

### Supplied material

```text
Alice session=A1, token=TA, profileId=A. Bob profileId=B. Names initially Alice and Bob. Role for Alice=member.
Rename input schema: profileId from route; body contains name only, 1..80 characters. Unknown fields reject with 400 atomically. Missing token -> 403; wrong owner -> 404; missing session -> 401.
Normal output uses escaped text.
R1: A1,TA,/profiles/A,{name:Ada & Lin}.
R2: A1,no token,/profiles/A,{name:Changed}.
R3: A1,TA,/profiles/B,{name:Changed}.
R4: A1,TA,/profiles/A,{name:Changed,role:admin}.
R5: A1,TA,/profiles/A,{name:<strong>Ada</strong>}.
Regression patch: API subrouter exempts all JSON endpoints from CSRF but still accepts A1 cookies.
```

### 1. Trace the normal lifecycle

Evaluate R1 and identify what authentication, authorization, binding and rendering each guarantee.

Worked answer: R1 succeeds and persists `Ada & Lin` for A only. Authentication establishes Alice; ownership chooses the permitted row; binding preserves the value as data; escaped rendering shows the ampersand correctly.

### 2. Isolate missing and wrong authority

Evaluate R2 and R3 against the complete middleware sequence.

Worked answer: R2 returns 403 with Alice unchanged. R3 returns 404 with Bob unchanged. A valid CSRF token does not authorize another profile, and valid ownership does not replace CSRF protection.

### 3. Inspect binding and output escape hatches

Evaluate R4/R5 and state the stored versus rendered representation.

Worked answer: R4 rejects with 400 and no partial name or role change. R5 stores the literal string `<strong>Ada</strong>` if names permit those characters and displays it as text, creating no strong element.

### 4. Assess the exemption patch

Explain why JSON content alone does not justify the broad exemption and name the concrete route regression.

Worked answer: The route still relies on ambient cookies. Its complete origin/content-type/CSRF design must be justified; merely declaring JSON is not a replacement. Repeating R2 on the API route must not mutate A, and supported frontend requests must send the documented token.

Deliverable: A lifecycle map and five exact request outcomes that expose bypasses introduced by route configuration.

Scope: Paper exercise: sessions, tokens, routes and policies are supplied. The express-api project below runs the same lifecycle checks against a real Express API.

## Executable practice

- [Secure a real HTTP API: project ZIP](https://softwaresecurity.com/downloads/projects/express-api-519f6cf7960d8d96.zip) — Node.js 22+ · npm

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [Spring Security CSRF integration](https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html)
- [Django: security in Django](https://docs.djangoproject.com/en/6.1/topics/security/). Django 6.1.
- [Django: deployment checklist](https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/). Django 6.1.
- [Django 6.0 release notes: Content Security Policy support](https://docs.djangoproject.com/en/6.0/releases/6.0/)
- [GitHub advisory GHSA-f82v-jwr5-mffw: Next.js middleware authorization bypass](https://github.com/advisories/GHSA-f82v-jwr5-mffw)
- [Express: production security best practices](https://expressjs.com/en/advanced/best-practice-security/)
- [Ruby on Rails security guide](https://guides.rubyonrails.org/security.html)