Skip to content

Content Security Policy

Content Security Policy (CSP) is an HTTP response header that controls which resources the browser is allowed to load for a page. It is the most effective defense against Cross-Site Scripting (XSS) attacks, preventing injected scripts from executing even if an attacker finds an injection point.

HyperDjango configures CSP through the SecurityHeadersMiddleware.

Quick Start

from hyperdjango.standalone_middleware import SecurityHeadersMiddleware

app.use(SecurityHeadersMiddleware(
    csp="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
))

This sets the Content-Security-Policy header on every response, restricting all resource loading to the same origin.

SecurityHeadersMiddleware

The SecurityHeadersMiddleware is a dataclass that adds security headers to all responses. CSP is one of several headers it manages.

from hyperdjango.standalone_middleware import SecurityHeadersMiddleware

security = SecurityHeadersMiddleware(
    content_type_nosniff=True,                       # X-Content-Type-Options: nosniff
    frame_options="DENY",                            # X-Frame-Options: DENY
    hsts=False,                                      # Strict-Transport-Security (off by default)
    hsts_max_age=31536000,                           # HSTS max-age in seconds (1 year)
    csp=None,                                        # Content-Security-Policy (off by default)
    referrer_policy="strict-origin-when-cross-origin",
    permissions_policy=None,                         # Permissions-Policy header
    cross_origin_opener_policy="same-origin",        # COOP header
)
app.use(security)

When csp is None (the default), no CSP header is sent. Set it to a policy string to enable.

The middleware uses setdefault on response headers, so per-response headers set by your handlers take priority over the middleware defaults.

CSP Directives

A CSP policy is a semicolon-separated list of directives. Each directive controls a specific resource type.

Common Directives

Directive Controls
default-src Fallback for all resource types not explicitly listed
script-src JavaScript (inline and external)
style-src CSS (inline and external)
img-src Images
font-src Web fonts
connect-src XHR, WebSocket, EventSource, fetch()
media-src Audio and video
object-src Plugins (Flash, Java) -- set to 'none'
frame-src Iframes
frame-ancestors What can embed this page in an iframe
base-uri Restricts <base> element URLs
form-action Restricts form submission targets

Source Values

Value Meaning
'self' Same origin only
'none' Block all
'unsafe-inline' Allow inline scripts/styles (weakens CSP significantly)
'unsafe-eval' Allow eval() and similar (avoid if possible)
'nonce-<base64>' Allow specific inline scripts by nonce
'strict-dynamic' Trust scripts loaded by already-trusted scripts
https: Any HTTPS origin
data: Data URIs
*.example.com Wildcard subdomain match
https://cdn.example.com Specific origin

Example Policies

app.use(SecurityHeadersMiddleware(
    csp=(
        "default-src 'self'; "
        "script-src 'self'; "
        "style-src 'self'; "
        "img-src 'self' data:; "
        "font-src 'self'; "
        "object-src 'none'; "
        "frame-ancestors 'none'; "
        "base-uri 'self'; "
        "form-action 'self'"
    )
))

API-Only Application

APIs serving JSON do not need to load any browser resources:

app.use(SecurityHeadersMiddleware(
    csp="default-src 'none'; frame-ancestors 'none'"
))

Application with CDN Assets

app.use(SecurityHeadersMiddleware(
    csp=(
        "default-src 'self'; "
        "script-src 'self' https://cdn.example.com; "
        "style-src 'self' https://cdn.example.com 'unsafe-inline'; "
        "img-src 'self' https://cdn.example.com https://images.example.com; "
        "font-src 'self' https://cdn.example.com; "
        "connect-src 'self' https://api.example.com; "
        "object-src 'none'"
    )
))

Application with Inline Scripts (Nonce-Based)

Instead of 'unsafe-inline', use nonces to allow specific inline scripts:

import secrets

@app.route("GET", "/page")
async def page(request):
    nonce = secrets.token_urlsafe(16)
    html = f"""
    <html>
    <head>
        <script nonce="{nonce}">
            console.log("This is allowed");
        </script>
    </head>
    <body>Hello</body>
    </html>
    """
    return Response.html(html, headers={
        "Content-Security-Policy": f"default-src 'self'; script-src 'nonce-{nonce}'"
    })

Per-response CSP headers override the middleware default, so you can set a nonce-specific policy on individual pages.

Report-Only Mode

To test a CSP policy without enforcing it, use the Content-Security-Policy-Report-Only header. Violations are reported but not blocked:

@app.route("GET", "/test-page")
async def test_page(request):
    return Response.html(html, headers={
        "Content-Security-Policy-Report-Only": (
            "default-src 'self'; "
            "report-uri /csp-report"
        )
    })

@app.route("POST", "/csp-report")
async def csp_report(request):
    report = await request.json()
    logger.warning("CSP violation", report=report)
    return Response(body=b"", status=204)

This lets you audit what would break before enforcing the policy. Once the report shows no unexpected violations, switch to enforcement by using the csp parameter on SecurityHeadersMiddleware.

Other Security Headers

SecurityHeadersMiddleware also sets these headers by default:

Header Default Purpose
X-Content-Type-Options nosniff Prevents MIME-type sniffing
X-Frame-Options DENY Prevents clickjacking
Referrer-Policy strict-origin-when-cross-origin Controls referrer information
Cross-Origin-Opener-Policy same-origin Isolates browsing context

Enable HSTS for HTTPS-only sites:

app.use(SecurityHeadersMiddleware(
    hsts=True,
    hsts_max_age=31536000,  # 1 year
    csp="default-src 'self'"
))

See Also

  • Security Guide -- comprehensive security configuration
  • CSRF -- cross-site request forgery protection
  • Middleware -- middleware stack and ordering