docs: rewrite Web Security category intro

The intro predated the audit and never mentioned nh3, which the README now lists.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Vinta Chen
2026-10-02 16:37:46 +08:00
co-authored by Claude
parent 1698a34c10
commit dcd7ac6941
+7 -10
View File
@@ -1,16 +1,13 @@
Every response your app sends should carry security headers, and a Python web security library like secure defines them once for Django, Flask, or FastAPI.
Sanitize HTML your users write with nh3, then add a Python web security library for response headers. secure sets CSP and HSTS once for the whole app.
How to choose:
- FastAPI or Starlette: secure's ASGI middleware
- Django, Flask, and other frameworks: secure in a response hook or middleware
- Most apps: the default BALANCED preset
- Tighter CSP and stronger isolation: the STRICT preset
- User-written HTML, like comments and posts, or code moving off bleach: nh3
- HTML rendered from user Markdown: nh3, run on the rendered output
- Security headers like CSP and HSTS, one policy across ASGI and WSGI apps: secure
secure lets you [keep one `Secure` policy object instead of scattered header strings](https://typeerror.com/secure/), and applies it through ASGI middleware, WSGI middleware, or your framework's response hooks. Start with `Secure.with_default_headers()`, which matches `Preset.BALANCED`, [the recommended default for most applications](https://github.com/TypeError/secure/blob/main/docs/usage.md#start-with-a-preset). [Review the headers it generates, and move stricter only when needed](https://typeerror.com/secure/#presets): `Preset.STRICT` tightens the CSP and isolation.
nh3 is a Python binding to ammonia, an allowlist-based HTML sanitizer built to stop cross-site scripting from untrusted user HTML. It parses fragments the same way browsers do. Call [`nh3.clean()` on each fragment](https://nh3.readthedocs.io/en/latest/#usage), and pass `tags` and `attributes` to set what it allows, like `tags={"b"}`. When you use the same options everywhere, create an [`nh3.Cleaner`](https://nh3.readthedocs.io/en/latest/#nh3.Cleaner) once and reuse it. ammonia won't linkify bare URLs or add line breaks, so its docs say to [run a markup processor before the sanitizer](https://github.com/rust-ammonia/ammonia#html-sanitization): render Markdown first, then clean the HTML it outputs.
The framework guide says to [prefer middleware when your framework makes it easy](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#how-to-choose-an-integration-style) and you want app-wide coverage. On FastAPI, [add `SecureASGIMiddleware` with `app.add_middleware()`](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#fastapi). On Flask, [call `set_headers(response)` in an `after_request` hook](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#flask). On Django, write [a small Django middleware class](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#django) that calls `set_headers()` and register it in `MIDDLEWARE`. For a framework the guide doesn't cover, [configure one `Secure` instance and apply it to the response as late as possible](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#custom-frameworks) before it's sent.
secure keeps all your security headers in one policy object, instead of header strings copied across handlers and hooks. Start from `Secure.with_default_headers()`, which its docs call [the recommended starting point](https://github.com/TypeError/secure/blob/main/docs/usage.md#start-with-a-preset), configure it once, and reuse it everywhere. [Prefer its ASGI or WSGI middleware](https://github.com/TypeError/secure/blob/main/docs/frameworks.md#how-to-choose-an-integration-style) to cover the whole app, and set headers per response when you're inside an existing hook or view. The defaults are only a starting point: [adjust the Content Security Policy](https://github.com/TypeError/secure#why-use-secure) for the scripts, styles, and third-party services your app really uses. Then [test a stricter policy against the real app](https://github.com/TypeError/secure/blob/main/docs/usage.md#build-an-explicit-configuration) before rollout.
The defaults are [a starting point, not a substitute for a review against your own app](https://typeerror.com/secure/). Adjust the Content Security Policy in particular for the scripts, styles, assets, and third-party services your app actually uses. Build it with the `ContentSecurityPolicy` builder instead of a header string, and [test a stricter policy against the real app before rollout](https://github.com/TypeError/secure/blob/main/docs/usage.md#build-an-explicit-configuration).
Security headers help the browser enforce transport, embedding, and content-loading rules, but they [don't replace output encoding, CSRF protection, authentication, or input validation](https://github.com/TypeError/secure/blob/main/docs/security_considerations.md). For web security materials beyond Python libraries, see [awesome-web-security](https://github.com/qazbnm456/awesome-web-security).
Use both, with sanitizing as the main defense. OWASP says to [sanitize HTML when users author it](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#html-sanitization), and to treat CSP as [an additional layer of defense](https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#other-controls), never the primary one.