Advanced CSP Demo

The basic demo covers which hosts may serve scripts, styles, images and frames. This page covers the directives that don't behave the way that mental model predicts.

Directives that do not inherit from default-src

Each pair below is the same policy with and without the directive under test.

Directives that cover more than you think

Response headers being sent

content-security-policy: default-src 'self' 'unsafe-inline' data: https://picsum.photos https://fastly.picsum.photos; connect-src 'self' https://ad.doubleclick.net;

Violations reported by this page
none yet

form-action submit to test

What happens: The form sends its fields to example.com. With form-action absent, a new tab opens and the submitted values appear in its URL. With form-action 'self', the browser cancels that navigation and records a violation.

Why: Form submission is a navigation, not a resource fetch. default-src therefore does not restrict it; an explicit form-action directive does.

Opens in a new tab so you don't lose your place. Check the address bar of that tab — the values went with it.


base-uri

What happens: Both cases inject <base href="https://picsum.photos/">. With base-uri absent, the browser accepts it and resolves every relative URL against that origin. With base-uri 'none', the browser ignores the element and records a violation.

Why: default-src can restrict the requests produced after URLs are resolved, but it does not control which document base is accepted. That is the separate job of base-uri.

Watch it: The image below is written as <img src="images/moustache-cakes.jpeg">. Its resolved URL is:

relative-path image

On the hijacked page, scroll down: the worker and <object> sections have broken too. Even root-relative URLs like /demo/assets/worker.js resolve against the injected base's origin, so the one element reroutes assets across the page.


frame-ancestors open the framing test

What happens: The framing test acts as an attacker's page. It can embed the version with frame-ancestors absent, but frame-ancestors 'none' makes the browser refuse the iframe.

Why: frame-ancestors is enforced by the page being framed and controls who may embed it. default-src controls what that page may load, so it provides no clickjacking protection. This directive must be sent in a response header; browsers ignore it in a <meta> policy.

Framing test: absent Framing test: 'none'


connect-src click to test

What happens: The active policy allows this page's nonced script to run, but clicking the button still blocks its request to ad.doubleclick.net.

Why: script-src controls whether code may run. A beacon, fetch(), XHR, EventSource or WebSocket is governed separately by connect-src, which this case limits to 'self'.

The violation fires before the network request, so this works with no internet connection.


worker-src

What happens: This page always tries to start a same-origin worker from the same URL. Under child-src 'self' it runs because worker-src is absent. Under worker-src 'none' the otherwise identical worker is blocked.

Why: The browser uses the first directive present in the worker fallback chain:
worker-src → child-src → script-src → default-src. A specific worker-src therefore replaces, rather than combines with, the broader fallbacks.

Note: a worker cannot be loaded cross-origin in the first place — that's same-origin policy, not CSP. This is about whether workers run at all, not about which host they come from.

This section also reports blocked under sandbox, for an unrelated reason: a sandboxed opaque origin can't load a same-origin script.


script-src-attr vs -elem click the button

What happens: Under this policy, the page's <script> blocks run, but the button's inline onclick= does not. Clicking still updates the status badge because an allowed script also installed a separate addEventListener() handler.

Why: script-src-elem governs script elements, while script-src-attr governs inline event attributes. This policy allows the former and sets the latter to 'none'.

(handler has not run)


object-src

What happens: Both boxes load same-origin documents that contain script. Under object-src absent, default-src 'self' permits both documents and their scripts run. Under object-src 'none', neither document loads.

Why: The parent page's script-src does not govern a document loaded through <object> or <embed>; the load is selected by object-src. This matters when users can place active HTML or SVG files on your origin, such as through an upload or attachment.

<object type="text/html">

<embed type="image/svg+xml"> — an <img> would not run this script.


Two CSP headers

What happens: Under this case, the first CSP header allows picsum.photos; the second does not. The external image is therefore blocked.

Why: Browsers enforce every CSP header as a separate policy. A request must satisfy all of them, so their combined effect is an intersection. Adding a source to Drupal's policy cannot relax another policy still being sent by a CDN or web server.

external image

upgrade-insecure-requests

What happens: The image markup contains an http:// URL. On an HTTPS page, upgrade-insecure-requests rewrites that request to https:// before it leaves the browser.

Why: This document directive upgrades insecure subresource URLs; it does not use default-src as a fallback. Modern browsers already auto-upgrade some mixed-content types, including images, so the no-CSP comparison may look the same. The Network panel shows the URL actually requested.

http image

sandbox

What happens: This case sends sandbox allow-scripts. JavaScript still runs, but the document receives an opaque origin, loses same-origin storage access, and cannot submit forms or open popups.

Why: CSP's sandbox applies the restrictions of an <iframe sandbox> to the whole document. It starts with every sandbox restriction enabled; allow-scripts removes only the script restriction. There is no allow-same-origin, allow-forms or allow-popups token here.

Document origin:

While this case is active, scroll back up and press the form-action submit button. Nothing happens at all — no navigation, no new tab, and the status badge never even changes, because the submission is blocked before the submit event fires. Silent failure is the thing to recognise here.


Every case changes one directive against the same permissive baseline, so the directive under test is the only variable.