CSP for YouTube embeds

frame-src https://www.youtube.com https://www.youtube-nocookie.com; script-src https://www.youtube.com

A YouTube embed needs frame-src https://www.youtube.com, and https://www.youtube-nocookie.com as well if you use privacy-enhanced mode. The IFrame Player API adds script-src https://www.youtube.com, which serves both the iframe_api script and the player script it loads. A plain <iframe> embed needs no script-src at all.

The policy

Content-Security-Policy: default-src 'self'; script-src 'self' https://www.youtube.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

The builder finds one low point: a <meta> tag ignores frame-ancestors, so send this as a header. Its table shows the chain frame-src → child-src → default-src: without frame-src, default-src 'self' decides, and the player is refused.

Only the frame is yours

The video, thumbnails and the player’s own requests load inside YouTube’s document, and that document answers to YouTube’s policy, not yours. So media-src, img-src and connect-src don’t need YouTube hosts for an embed. They do if your page fetches something itself, such as a thumbnail for a click-to-play placeholder.

Privacy-enhanced mode

YouTube’s help page says to change the embed domain from https://www.youtube.com to https://www.youtube-nocookie.com. The frame then comes from the second host, so list whichever you embed; list both while you switch.

<iframe src="https://www.youtube-nocookie.com/embed/M7lc1UVf-VE" width="640" height="360" allow="autoplay; encrypted-media" allowfullscreen></iframe>

The IFrame API with a nonce

script-src 'nonce-REPLACE_ME' 'strict-dynamic' https://www.youtube.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com

YouTube’s embed and IFrame API docs say nothing about CSP. The loader in their getting-started sample is an inline script that creates the iframe_api tag, so with 'strict-dynamic' that inline script needs only your nonce, and the tag it creates is trusted. The iframe_api script served today also copies its own nonce, if it has one, onto the player script it adds. The host stays as a fallback for browsers without 'strict-dynamic'; frame-src stays as it is, because 'strict-dynamic' covers scripts only.

Gotchas

  • frame-ancestors is the other direction. It says who may frame your page; it does nothing for the videos you embed.
  • Trusted Types. iframe_api creates a policy named youtube-widget-api. A trusted-types list that leaves it out means the player script can’t be set once require-trusted-types-for 'script' is on.
  • The origin parameter. With enablejsapi=1, YouTube asks you to pass your page’s origin, so other scripts can’t take over the player. That is YouTube’s check, separate from your CSP.

In Apache

Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://www.youtube.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"

Try it in the CSP Builder

everything that is fetched and has no directive of its own
scripts, inline and loaded
frames and iframes
<object>, <embed> and <applet>
the URLs a <base> element may point at
who may frame this page

A nonce has to be fresh for every response, so 'nonce-REPLACE_ME' is a placeholder: your server fills it in and puts the same value in the <script nonce=…> tag.

1 finding worst low

6 directives

Content-Security-Policy

default-src 'self'; script-src 'self' https://www.youtube.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; frame-ancestors 'self'

The same policy as a meta tag

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://www.youtube.com; frame-src https://www.youtube.com https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'">

A meta tag only works in HTML, has to be in the <head>, and is read once: a header can be changed per response.

Findings · worst low

  • low A <meta> tag ignores frame-ancestors. If this policy goes into HTML, that directive is doing nothing there: send it as a header.

Every directive, in plain words

DirectiveWhat it governsSources
default-srceverything that is fetched and has no directive of its own
  • 'self' this origin only
script-src
→ default-src
scripts, inline and loaded
  • 'self' this origin only
  • https://www.youtube.com the host www.youtube.com over https
frame-src
→ child-src → default-src
frames and iframes
  • https://www.youtube.com the host www.youtube.com over https
  • https://www.youtube-nocookie.com the host www.youtube-nocookie.com over https
object-src
→ default-src
<object>, <embed> and <applet>
  • 'none' nothing of this kind may load
base-urithe URLs a <base> element may point at
  • 'self' this origin only
frame-ancestorswho may frame this page
  • 'self' this origin only

Checked against CSP Level 3: every directive, every source keyword, and the fallback chain that carries a directive to default-src. Nothing is uploaded, and no request is made to the site whose policy you paste.

Runs in your browser. Nothing you enter is sent anywhere.