Embedding YouTube looks simple right up until CSP starts blocking half of it.

You add an <iframe>, refresh the page, and suddenly the browser console is yelling about frame-src, img-src, or some random googlevideo.com request you didn’t explicitly ask for. That’s normal. YouTube embeds pull in more than just youtube.com, and playlists add a couple more moving parts.

Here’s the practical reference I wish I had the first few times I locked this down.

The short version

For a basic YouTube embed, you usually need:

  • frame-src for the iframe itself
  • img-src for thumbnails and related images
  • sometimes script-src if you use the YouTube Iframe API
  • sometimes connect-src if your page code talks to YouTube or related endpoints

The core domains are usually:

  • https://www.youtube.com
  • https://www.youtube-nocookie.com
  • https://i.ytimg.com
  • https://img.youtube.com

If you use privacy-enhanced mode, prefer youtube-nocookie.com.

Minimal CSP for a normal YouTube embed

If your page already has a sensible baseline policy, this is the smallest useful addition:

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://www.youtube.com;
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  object-src 'none';
  base-uri 'self';

And the HTML:

<iframe
  width="560"
  height="315"
  src="https://www.youtube.com/embed/dQw4w9WgXcQ"
  title="YouTube video player"
  loading="lazy"
  allowfullscreen>
</iframe>

That’s enough for many cases.

CSP for privacy-enhanced mode

If you use YouTube’s privacy-enhanced embed URLs, swap in youtube-nocookie.com. I recommend this by default unless you have a reason not to.

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://www.youtube-nocookie.com;
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  object-src 'none';
  base-uri 'self';

Embed example:

<iframe
  width="560"
  height="315"
  src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
  title="YouTube video player"
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"
  allowfullscreen>
</iframe>

If you want the least annoying setup for marketing sites, docs sites, and blogs, this is usually it.

CSP for YouTube playlists

Playlists don’t need a fundamentally different CSP. The iframe still comes from YouTube or YouTube No-Cookie. The main difference is the embed URL.

Playlist by playlist ID

<iframe
  width="560"
  height="315"
  src="https://www.youtube-nocookie.com/embed/videoseries?list=PL1234567890ABCDEFG"
  title="YouTube playlist player"
  loading="lazy"
  allowfullscreen>
</iframe>

CSP:

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://www.youtube-nocookie.com;
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  object-src 'none';
  base-uri 'self';

Single video with a playlist context

<iframe
  width="560"
  height="315"
  src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ?list=PL1234567890ABCDEFG"
  title="YouTube playlist video"
  loading="lazy"
  allowfullscreen>
</iframe>

Same policy.

If you use the YouTube Iframe API

This is where people usually overshoot and allow too much.

If you load the API script:

<script src="https://www.youtube.com/iframe_api"></script>
<div id="player"></div>
<script nonce="{{ .CSPNonce }}">
  function onYouTubeIframeAPIReady() {
    new YT.Player('player', {
      videoId: 'dQw4w9WgXcQ',
      playerVars: {
        listType: 'playlist',
        list: 'PL1234567890ABCDEFG'
      }
    });
  }
</script>

Your CSP needs to allow:

  • the script from www.youtube.com
  • the frame source
  • thumbnails/images
  • your inline script via nonce or hash

Example:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-abc123' https://www.youtube.com;
  frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  object-src 'none';
  base-uri 'self';

If you’re still allowing 'unsafe-inline' in script-src, that’s the part I’d fix first. Use a nonce or hash instead. If you want a deeper breakdown of nonce-based policies and directive behavior, csp-guide.com is a good reference.

A stricter real-world starting point

A lot of production CSPs start from an existing locked-down policy and add YouTube on top. For example, this real policy from HeaderTest is already pretty tight:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-NzU4NDliYmMtZDU2Ni00MjMxLWI4NGEtMDdkZTFhNDRjNjU4' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'

To support YouTube embeds there, I’d minimally change frame-src like this:

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-NzU4NDliYmMtZDU2Ni00MjMxLWI4NGEtMDdkZTFhNDRjNjU4' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
  frame-src 'self' https://consentcdn.cookiebot.com https://www.youtube.com https://www.youtube-nocookie.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

That works because img-src 'self' data: https: already allows YouTube thumbnails over HTTPS. If you want to inspect a policy and catch obvious mistakes quickly, HeaderTest is handy.

Copy-paste policies

1. Basic YouTube embed

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://www.youtube.com;
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  object-src 'none';
  base-uri 'self';

2. Privacy-enhanced embed

Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://www.youtube-nocookie.com;
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  object-src 'none';
  base-uri 'self';
Content-Security-Policy:
  default-src 'self';
  frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  object-src 'none';
  base-uri 'self';

4. YouTube Iframe API with nonce

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-REPLACE_WITH_NONCE' https://www.youtube.com;
  frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  object-src 'none';
  base-uri 'self';

Common CSP errors and what they mean

Refused to frame ... because it violates frame-src

Your iframe domain isn’t allowed.

Fix:

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

Refused to load the image ... because it violates img-src

Usually thumbnail URLs from i.ytimg.com or img.youtube.com are blocked.

Fix:

img-src 'self' data: https://i.ytimg.com https://img.youtube.com;

Refused to load the script 'https://www.youtube.com/iframe_api'

You’re using the JS API but didn’t allow the script source.

Fix:

script-src 'self' https://www.youtube.com 'nonce-REPLACE_WITH_NONCE';

The embed works locally but fails in production

Classic reasons:

  • production CSP is stricter than your local setup
  • an edge/CDN layer injects or rewrites headers
  • you tested a normal embed locally and switched to youtube-nocookie.com later
  • your page has frame-src but also a legacy child-src rule causing confusion in older setups

Don’t forget frame-ancestors

This one doesn’t control your ability to embed YouTube. It controls who can embed your page.

If you don’t want your page framed elsewhere:

frame-ancestors 'none';

If your app needs to be embedded by a parent app, configure that explicitly. I’ve seen teams accidentally break partner integrations by cargo-culting frame-ancestors 'none' into everything.

My default recommendation

If I’m adding YouTube to a typical content site, I start here:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-REPLACE_WITH_NONCE';
  style-src 'self';
  img-src 'self' data: https://i.ytimg.com https://img.youtube.com;
  frame-src 'self' https://www.youtube-nocookie.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

Then I widen only if the implementation really needs it.

That’s the pattern that keeps CSP manageable: start strict, add the exact YouTube sources you actually use, and don’t whitelist half of Google just because one iframe looked noisy in DevTools.