Content Security Policy
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06), Python 3.12-3.14
and PostgreSQL 15 or newer, all checked on 2026-10-03; the 5.2 LTS (5.2.17) has no built-in Content Security Policy,
so everything below needs 6.0 or newer, and the text says which parts need 6.1. Every example was run in a scratch
project (catalog app, SQLite, Django test client, runserver and curl); none needs PostgreSQL or Redis. Nonce
values in the listings are written as <nonce> because a real one is different on every response.
A Content Security Policy (CSP) is an HTTP response header with which the server tells the browser which sources
scripts, styles, images, frames and other resources may come from. It does not replace output escaping (see
Security); it is a second line of defence that limits the damage when an escaping bug
lets an attacker inject markup, because the browser refuses to run an injected inline script that the policy does not
allow. Until Django 6.0 this needed a third-party package; since 6.0 it is a middleware, two settings, a small enum of
constants and two view decorators, and 6.1 added the csp_nonce_attr template tag. The sections below cover the
header model, the middleware and settings, the constants, nonces, per-view overrides, violation reports, the caching
pitfall, a rollout plan for the Bookshelf application, and the move from django-csp.
In a hurry
| Need | Rule of thumb | Section |
|---|---|---|
Turn CSP on |
Add |
|
Spell source keywords |
Use |
|
Allow specific inline code |
Put |
|
Change one view |
|
|
Collect violations |
Add |
|
Cache pages |
Never cache full responses that contain a nonce. |
|
Leave django-csp |
Dict |
How a CSP works
The standard defines two complementary headers. Content-Security-Policy is enforced: the browser blocks whatever
the policy does not allow. Content-Security-Policy-Report-Only uses the same syntax but only reports violations
(to the developer console and, if the policy names a reporting directive, to an endpoint) and blocks nothing. A policy
is a semicolon-separated list of directives, each a name followed by source expressions:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<nonce>'; img-src 'self' data:; object-src 'none'
The fetch directives (script-src, style-src, img-src, font-src, connect-src, frame-src, …) each govern
one kind of resource; default-src is the fallback for the fetch directives that are not listed. There is no
inheritance between a specific directive and default-src: once img-src is present, default-src is ignored for
images. Other directives restrict something else: base-uri, form-action, frame-ancestors (who may embed the
page, the modern replacement for X-Frame-Options, see Security), object-src and
reporting directives such as report-uri. Django treats all of this as opaque data: it assembles the header from your
settings and does not check that the directive names or values are valid, so the browser’s developer tools or an
external validator are the checker.
Source: Content Security Policy: Overview and SECURE_CSP
The middleware and the two settings
Enabling CSP takes two steps: add the middleware and describe the policy. The middleware lives in
django.middleware.csp and, like every Django middleware, wraps the request cycle (see
Middleware); it creates the lazy nonce holder on the way in and attaches the headers
on the way out. The documentation does not prescribe a position in the list; the example keeps it right after
SecurityMiddleware so that it sees the final response of everything below it.
from django.utils.csp import CSP
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",
]
SECURE_CSP = {
"default-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:"],
"object-src": [CSP.NONE],
"base-uri": [CSP.NONE],
"frame-ancestors": [CSP.NONE],
"form-action": [CSP.SELF],
}
SECURE_CSP_REPORT_ONLY = {
"default-src": [CSP.SELF],
"report-uri": "/csp-reports/",
}
The rules that the middleware follows, checked against its source and the reference page:
-
SECURE_CSP(default{}) is the enforced policy andSECURE_CSP_REPORT_ONLY(default{}) the report-only one. Either, both or neither may be set; an empty mapping means "no header of that kind". -
Each setting must be a mapping of directive name to value. A value is normally a list, tuple or set of source expressions; a set is sorted so the header is identical on every response (which keeps caches and browsers happy). A single string is accepted as one value.
Truerenders a directive that takes no value (for example"upgrade-insecure-requests": True), andNoneorFalseleaves the directive out, which is handy for switching one off from an environment variable. -
The middleware never overwrites a header that is already on the response, so a view, another middleware or a proxy rule that sets
Content-Security-Policyitself wins. -
The check framework validates the shape (both settings must be dictionaries) and, in 6.1, warns with
security.W027about a missing context processor (next sections).manage.py checkpassed on the project above.
Use SECURE_CSP alone for a policy you have already tested, SECURE_CSP_REPORT_ONLY alone to trial a new policy
without risk (it protects nothing), and both to keep an enforced baseline while you experiment with a stricter
candidate; collecting reports on a mature policy also surfaces regressions and tampering. The headers the project sent,
captured with curl against runserver:
curl -sI http://localhost:8000/books/ | grep -i content-security
Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports/
The CSP constants
Several source expressions are keywords that must be written with single quotes inside the header, which is easy to
get wrong (["self"] is a host called "self", not the keyword). django.utils.csp.CSP is a string enum that carries
the correctly quoted values, so a typo becomes an AttributeError at start-up instead of a silently broken policy.
| Constant | Renders as | Meaning |
|---|---|---|
|
|
Block everything for that directive. |
|
|
Same origin (scheme, host and port). |
|
|
Allow inline scripts and styles and |
|
|
Allow |
|
|
Allow inline event handlers whose hash is listed (CSP Level 3). |
|
|
Allow compiling WebAssembly without |
|
|
Trust scripts loaded by an already trusted (nonce or hash) script. |
|
|
Include a sample of the offending code in reports (it may expose sensitive data). |
|
placeholder |
Django-specific marker replaced per request by |
The enum also holds the two header names (CSP.HEADER_ENFORCE, CSP.HEADER_REPORT_ONLY), which are handy in tests.
Hosts, schemes and wildcards stay plain strings ("data:", "https://covers.example.com").
Nonces: allowing specific inline code
The strictest practical policy bans inline code entirely and loads everything from files under 'self'. When inline
<script> or <style> elements cannot be avoided, a nonce (number used once) lets exactly those elements run
without 'unsafe-inline': the server generates a random value per response, puts 'nonce-<value>' in the header and
the same value in the nonce attribute of the permitted elements. An injected script cannot guess the value, so it
stays blocked.
Three pieces are needed; leaving out any one of them disables the protection, which the 6.1 security.W027 check
now flags for the second.
-
Policy: add
CSP.NONCEtoscript-srcand/orstyle-src(the reference page says the placeholder only applies to those directives). -
Context processor: add
django.template.context_processors.csptocontext_processors; it puts the nonce in the template context ascsp_nonce. -
Templates: put the nonce on each inline element, and on external ones when the policy has no
'self'for them.
SECURE_CSP = {
"default-src": [CSP.SELF],
"script-src": [CSP.SELF, CSP.NONCE],
"style-src": [CSP.SELF, CSP.NONCE],
"img-src": [CSP.SELF, "data:"],
"object-src": [CSP.NONE],
"base-uri": [CSP.NONE],
"frame-ancestors": [CSP.NONE],
"form-action": [CSP.SELF],
}
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.csp",
],
},
},
]
In templates (see Templates) use the variable for inline elements and, since 6.1, the
csp_nonce_attr tag for elements with a src or href. The tag is a built-in, so there is nothing to {% load %};
it renders nonce="<value>" when the context processor is active and an empty string otherwise, and it was verified
in django/template/defaulttags.py of 6.1.1.
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Books</title>
<link rel="stylesheet" href="{% static 'catalog/site.css' %}" {% csp_nonce_attr %}>
<style nonce="{{ csp_nonce }}">
.book { font-weight: bold; }
</style>
</head>
<body>
<ul>
{% for book in books %}<li class="book">{{ book }}</li>{% endfor %}
</ul>
<script src="{% static 'catalog/site.js' %}" {% csp_nonce_attr %}></script>
<script nonce="{{ csp_nonce }}">
document.title = "Bookshelf";
</script>
</body>
</html>
The rendered response (nonce replaced by a placeholder) and its header:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-<nonce>'; style-src 'self' 'nonce-<nonce>'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'
<link rel="stylesheet" href="/static/catalog/site.css" nonce="<nonce>">
<style nonce="<nonce>">
<script src="/static/catalog/site.js" nonce="<nonce>"></script>
<script nonce="<nonce>">
Details worth knowing, all verified against the 6.1.1 source:
-
Lazy generation. The middleware attaches a
LazyNonceto every request, but the value (secrets.token_urlsafe(16)) is only produced when something reads it, typically{{ csp_nonce }}. If nobody reads it,CSP.NONCEis silently removed from the header (a directive that contained only the placeholder disappears), so pages without inline code carry a nonce-free header. TheLazyNonceis truthy only after it was evaluated; that is howcsp_nonce_attrdecides whether to render anything, and how libraries can support CSP without requiring it. -
Same value everywhere. The enforced and the report-only header of one response share the request’s nonce.
-
Header without markup, or markup without header. If the template prints a nonce but the policy lacks
CSP.NONCE, the HTML has anonceattribute but the header has no matching source, so the browser blocks the inline element (or reports it in report-only mode). The inverse, the policy listingCSP.NONCEwith no context processor, produces a header nonce that no element carries; that is the casesecurity.W027warns about. -
Media objects. In 6.1, passing a
Mediaobject renders every asset with the nonce (see Form rendering and widgets):
{% csp_nonce_attr form.media %}
With a form whose Media lists catalog/search.js and catalog/search.css, the tag rendered
<link href="/static/catalog/search.css" media="all" nonce="<nonce>" rel="stylesheet"> followed by
<script src="/static/catalog/search.js" nonce="<nonce>"></script>; the tag stands in for {{ form.media }} rather than
decorating it.
Source: Nonce config (the Media form of the tag)
The admin and built-in templates
The admin has inline scripts and styles, so a script-src policy without 'unsafe-inline' breaks it unless it carries
a nonce. Django 6.1 adds the nonce attribute to <script>, <style> and <link> elements in the admin templates and
in all built-in templates when the csp() context processor is configured. The test client confirmed it on the project
above: the admin login page’s header contained a nonce and the HTML carried the same value. Per the 6.1 release note, before 6.1 the admin templates
did not add nonces, so a strict policy there needs a looser per-view policy or CSP.UNSAFE_INLINE for the admin URLs.
Admin customisation that adds your own inline code (see Admin customisation)
should use the same two template mechanisms as any other page.
Source: Django 6.1 release notes: CSP
Per-view overrides
The global settings are right for most pages, but a payment widget, an embeddable page or an API endpoint can need a
different policy. Two decorators in django.views.decorators.csp replace the policy for one view:
-
csp_override(config)replaces theContent-Security-Policyheader withconfig, which has the same format asSECURE_CSP. -
csp_report_only_override(config)does the same forContent-Security-Policy-Report-Only.
Both replace the base policy completely: nothing is merged with SECURE_CSP. An empty mapping {} adds no header of
that kind to the response, effectively disabling it for that view. Because the middleware does not overwrite existing
headers and consults the override before the setting, overriding the enforced header leaves the report-only header
untouched and vice versa.
from django.http import HttpResponse, JsonResponse
from django.utils.csp import CSP
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csp import csp_override, csp_report_only_override
@csp_override(
{
"default-src": [CSP.SELF],
"img-src": [CSP.SELF, "data:", "https://covers.example.com"],
}
)
def book_detail(request, pk):
return HttpResponse(f"Book {pk}")
@csp_override({})
def widget_embed(request):
return HttpResponse("embeddable widget")
@csp_report_only_override(
{"default-src": [CSP.SELF], "report-uri": "/csp-reports/"}
)
def experimental(request):
return HttpResponse("report-only override")
@method_decorator(csp_override({"default-src": [CSP.NONE]}), name="dispatch")
class ApiPing(View):
def get(self, request):
return JsonResponse({"ok": True})
The observed headers (test client and curl): /books/1/ sent only
default-src 'self'; img-src 'self' data: https://covers.example.com (no script-src, no nonce) plus the untouched
report-only header; /widget/ sent no Content-Security-Policy at all but still the report-only header;
/experimental/ kept the global enforced header and replaced the report-only one with
default-src 'self'; report-uri /csp-reports/; /api/ping/ sent default-src 'none', a sensible policy for a JSON
endpoint (see Class-based views for decorating dispatch).
Weakening or disabling the policy on one page can compromise the whole site, because of the same-origin policy: an attacker who exploits a vulnerability on the weak page can reach the rest of the origin. Keep overrides few and reviewed.
Extending instead of replacing
Because an override replaces everything, a per-view policy that only wants one extra image host must repeat the rest
or derive from the base. A small helper that copies SECURE_CSP and appends to a directive keeps one source of truth
(it is application code, not a Django API; the CSP.NONCE placeholder is copied along and is still resolved by the
middleware).
from django.conf import settings
from django.shortcuts import render
def extend_policy(base, **extra):
"""Copy base and append values to the given directives."""
merged = {
directive: list(values) if isinstance(values, (list, tuple, set)) else values
for directive, values in base.items()
}
for directive, values in extra.items():
merged[directive] = [*merged.get(directive, []), *values]
return merged
@csp_override(extend_policy(settings.SECURE_CSP, **{"img-src": ["https://covers.example.com"]}))
def gallery(request):
return render(request, "catalog/book_list.html", {"books": ["Dune"]})
The test below asserts that the derived policy keeps the base directives and the nonce:
def test_extend_keeps_base_and_nonce(self):
response = self.client.get("/gallery/")
policy = response.headers["Content-Security-Policy"]
self.assertIn("img-src 'self' data: https://covers.example.com", policy)
self.assertIn("object-src 'none'", policy)
self.assertIn("'nonce-", policy)
Evaluating settings.SECURE_CSP at import time, as the decorator argument does, fixes the value when the module is
loaded, so override_settings(SECURE_CSP=…) in a test does not reach it; in a view that needs a runtime-dependent
policy, build the dictionary and call the decorator inside a function instead.
Violation reports
When a violation occurs browsers log it in the developer console. To receive reports on a server the policy must also
contain a reporting directive; the Django examples use report-uri. Django accepts such directives in the settings but
does not provide anything that receives, stores or analyses reports: you write the endpoint or use a monitoring
service. The reference page adds that, as MDN notes, a report-only policy without a reporting directive has no effect
beyond the console.
A minimal endpoint for the Bookshelf application accepts the application/csp-report POST body, logs the interesting
fields and answers 204. It has to be exempt from CSRF protection (browsers send the report without a token, see
Security), so it must not do anything state-changing with the payload; treat the body as
untrusted input (size, rate, logging injection).
import json
import logging
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
logger = logging.getLogger("catalog.csp")
@csrf_exempt
@require_POST
def csp_report(request):
try:
payload = json.loads(request.body or b"{}")
except ValueError:
return HttpResponse(status=400)
report = payload.get("csp-report", {})
logger.warning(
"CSP violation: %s blocked %s",
report.get("violated-directive"),
report.get("blocked-uri"),
)
return HttpResponse(status=204)
def test_report_endpoint(self):
body = '{"csp-report": {"violated-directive": "script-src", "blocked-uri": "inline"}}'
with self.assertLogs("catalog.csp", level="WARNING") as logs:
response = self.client.post(
"/csp-reports/", body, content_type="application/csp-report"
)
self.assertEqual(response.status_code, 204)
self.assertIn("script-src", logs.output[0])
Newer reporting uses the report-to directive and a separate Reporting-Endpoints header; Django sets neither header
for you, but since it does not validate directives you can put report-to in the policy and add the other header in
your own middleware or at the proxy. The 6.1 reference only documents report-uri, so check the CSP specification and
MDN before relying on report-to.
Caching and nonce reuse
A nonce is only a defence if it is unique per response. Full-page caching (the per-site cache, cache_page, a CDN)
stores the HTML with the nonce that was generated for the first visitor and serves it to everyone. The reference page
warns that the pages may still appear to work, since header and HTML match, but the nonce is now known and reused,
which defeats its purpose.
The scratch project showed a second, sharper symptom with cache_page: the cached HTML kept the first request’s
nonce, but the second response’s header had no nonce at all, because the middleware sits outside the cached view,
nothing read the nonce during that request, and the placeholder was dropped. The browser then blocks every inline
script and style on the cached page.
def test_cache_page_serves_stale_nonce(self):
first = self.client.get("/books/cached/")
second = self.client.get("/books/cached/")
stale = nonce_in_header(first)
# The cached HTML still carries the first nonce ...
self.assertIn(f'nonce="{stale}"', second.content.decode())
# ... but this response's header has no matching nonce at all.
self.assertIsNone(nonce_in_header(second))
(nonce_in_header is a test helper that extracts the value with a regular expression.) The options, in order of
preference: do not cache pages that contain a nonce; cache fragments that contain no nonce instead of whole pages; or remove inline scripts and styles
altogether, so that no nonce is needed and the page is cacheable.
Rolling out a policy
A CSP added to an existing site in one step usually breaks something: an analytics snippet, an inline onclick, a
font from a CDN, a third-party widget. A staged plan keeps the risk low.
-
Start with
SECURE_CSP_REPORT_ONLYonly, with the target policy and areport-uri, and watch the console and the report endpoint for a few days across the real pages (admin included). -
Fix the sources: move inline scripts and event handlers into files, add nonces where inline code must stay, list the CDN hosts you really use.
CSP.UNSAFE_INLINEforstyle-srcis a common, lesser compromise; forscript-srcit removes most of the benefit. -
Copy the policy into
SECURE_CSPand keep the report-only setting for the next candidate (stricter) policy. -
Tighten over time: remove
unsafe-*keywords, addobject-src 'none',base-uri 'none',frame-ancestorsandform-action.
Switching the whole thing per environment is a settings concern (see
Settings and configuration): reading an environment variable to choose
between the two settings, or to set SECURE_CSP = {} in a local development settings module where Vite or the debug
toolbar inject inline code, is ordinary Python.
import os
CSP_ENFORCE = os.environ.get("DJANGO_CSP_ENFORCE") == "1"
SECURE_CSP = BOOKSHELF_POLICY if CSP_ENFORCE else {}
SECURE_CSP_REPORT_ONLY = {} if CSP_ENFORCE else {**BOOKSHELF_POLICY, "report-uri": "/csp-reports/"}
Here BOOKSHELF_POLICY stands for the dictionary of the earlier listing. The page that
Static files and front-end assets describes is the usual home of
the sources the policy has to name, and the Referrer-Policy and Cross-Origin-Opener-Policy headers that complement
CSP come from SecurityMiddleware, covered in Security.
Position in the request cycle, sync and async
The middleware subclasses MiddlewareMixin, so it works under WSGI and ASGI without any async-to-sync adapter cost
(see Middleware and The request-response
cycle). process_request stores the lazy nonce on request; process_response runs when the response travels back,
reads the per-view override attributes the decorators placed on the response, falls back to the settings, and sets the
headers unless already present. Responses returned directly by a middleware above it (for example a redirect from
SecurityMiddleware) do not travel through it and carry no CSP header.
Migrating from django-csp
The third-party package django-csp (Mozilla) filled this gap before Django 6.0. Its 4.0 release (PyPI, 2025-04-02)
already moved to dictionary configuration, which makes the move to the built-in support mostly mechanical; older
CSP_* settings must first pass through its own 4.0 migration guide, which also notes that manage.py check prints a
draft dictionary from old settings. The mapping to Django’s built-in features:
| django-csp | Django 6.0/6.1 | Notes |
|---|---|---|
|
|
Remove the package, the app entry and its middleware. |
|
|
The |
|
|
Same shape. |
|
|
Both use a placeholder for the nonce. |
nonce in templates via the package’s context processor |
|
Replace the package’s template variable; 6.1 adds the tag and |
|
|
Django only replaces; reproduce "update" with a helper like Extending instead of replacing. |
|
|
Django has one decorator per header instead of a |
|
|
Per header, as above. |
|
none |
Django has no URL-prefix exclusion; use |
|
none |
Django has no sampling; implement it in the report endpoint if the volume is a problem. |
The steps: install nothing, remove csp from INSTALLED_APPS and its middleware, convert the settings (per the table
above), import CSP from django.utils.csp instead of csp.constants, add the context processor if you used nonces,
replace the decorators, swap the template variables, run manage.py check (on 6.1 security.W027 points to a missing
context processor) and the test suite, and finally pip uninstall django-csp. Subclassed CSPMiddleware code has no
counterpart in the documented API, so move the custom logic into a small middleware of your own or into the decorators. The django-csp column above reflects its 4.0
documentation; re-check it against the version you actually run.
Testing a policy
Header assertions belong in the test suite (see Security for the wider checklist). The
tests in this page’s scratch project run on the Django test client with manage.py test; all thirteen passed. A
compact subset:
import re
from django.test import TestCase
def nonce_in_header(response, header="Content-Security-Policy"):
match = re.search(r"'nonce-([^']+)'", response.headers[header])
return match.group(1) if match else None
class PolicyTests(TestCase):
def test_nonce_matches_html(self):
response = self.client.get("/books/")
nonce = nonce_in_header(response)
self.assertIsNotNone(nonce)
self.assertEqual(response.content.decode().count(f'nonce="{nonce}"'), 4)
def test_nonce_changes_per_request(self):
first = nonce_in_header(self.client.get("/books/"))
second = nonce_in_header(self.client.get("/books/"))
self.assertNotEqual(first, second)
def test_empty_override_disables_header(self):
response = self.client.get("/widget/")
self.assertNotIn("Content-Security-Policy", response.headers)
A test client cannot tell you whether a real browser would block something: use the browser’s console with the report-only policy for that.
Source: Content Security Policy: CSP constants (header names)
Pitfalls
-
Quoting.
"self"is not'self'; use theCSPconstants. -
default-srcis not inherited. Addingimg-srcremoves thedefault-srcrule for images. -
unsafe-inlineplus a nonce. Browsers that support nonces ignore'unsafe-inline'in the same directive; do not rely on it as a fallback. -
Inline event handlers (
onclick="…") are inline code and need'unsafe-hashes'or a refactor intoaddEventListener; a nonce on the element does not help. -
Nothing validates your policy. A misspelt directive name is sent as written and ignored by the browser.
-
Third-party tools. The Django debug toolbar and front-end dev servers inject inline code; loosen or disable the policy only in development settings.
-
Redirects, errors and static files. A response that the middleware never sees (served by a proxy, WhiteNoise before the middleware, or an early redirect) has no CSP header; check with
curl, not only in the browser. -
A header already set wins. Another layer that sets
Content-Security-Policysilently disables yours for that response.
What changed recently, and where the books are outdated
-
6.0: built-in CSP support: the middleware,
SECURE_CSP,SECURE_CSP_REPORT_ONLY, theCSPenum, thecsp()context processor,csp_overrideandcsp_report_only_override, thecsp_noncetemplate variable. -
6.1: the
csp_nonce_attrtemplate tag, includingMediarendering; thesecurity.W027check; nonce attributes in the admin and other built-in templates when the context processor is configured. -
django-csp 4.0 (2025-04-02): moved from
CSP_*settings to dictionary settings, which are close in shape to the built-in ones. -
The four books were written for Django 1.11 to 2.x and so predate all of this: none of them can describe the built-in middleware,
SECURE_CSPor nonce support, and a project following them would add CSP, if at all, through a third-party package or a web-server rule. Their security advice (escaping, CSRF,X-Frame-Options; see Security) remains valid and is complemented byframe-ancestorsin a policy.