YouTube embeds look simple until CSP gets involved.
The usual failure mode goes like this: you paste an <iframe> embed, add autoplay=1, ship a strict policy, and suddenly the video is blank or blocked in the console. Then someone “fixes” it by throwing https: into frame-src or loosening half the policy. That works, but it’s lazy and expensive from a security standpoint.
Here’s the version I’d actually ship for a developer-facing site.
What autoplay on YouTube really needs
For a basic YouTube embed, the browser loads a frame from YouTube. If your CSP blocks that origin in frame-src, the embed dies.
For autoplay specifically, there are two separate concerns:
- CSP must allow the iframe origin
- Browser autoplay rules must allow playback
CSP only handles the first part. If autoplay still doesn’t start, that’s usually because the browser blocks unmuted autoplay.
For YouTube embeds, the common pattern is:
- use
https://www.youtube.com/embed/VIDEO_ID - add
?autoplay=1&mute=1 - optionally add
allow="autoplay; encrypted-media; picture-in-picture"
mute=1 is the difference between “works in most browsers” and “why does this only autoplay on my machine?”
The minimum HTML embed
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/M7lc1UVf-VE?autoplay=1&mute=1&playsinline=1"
title="YouTube video player"
allow="autoplay; encrypted-media; picture-in-picture"
referrerpolicy="strict-origin-when-cross-origin"
loading="lazy"
allowfullscreen>
</iframe>
A few opinions here:
- I almost always include
mute=1for autoplay. playsinline=1helps on mobile.loading="lazy"is great, but remember lazy loading delays the frame until near viewport. If you expect instant autoplay above the fold, test that behavior.allow="autoplay"is not a CSP setting. It’s a permissions policy hint for the iframe.
The CSP directive that matters most: frame-src
If your page embeds YouTube in an iframe, frame-src is the main directive you need.
A tight policy might look like this:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self' https://www.youtube.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
That allows your own frames plus YouTube embeds from www.youtube.com.
If you use YouTube’s privacy-enhanced mode, the iframe origin changes.
Privacy-enhanced mode: youtube-nocookie.com
A lot of teams prefer the no-cookie embed domain:
<iframe
src="https://www.youtube-nocookie.com/embed/M7lc1UVf-VE?autoplay=1&mute=1"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen>
</iframe>
Then your CSP must allow that domain instead:
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://www.youtube-nocookie.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
If your site uses both embed formats, allow both explicitly:
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
I’d rather list both than slap https: in there and call it done.
Don’t cargo-cult script-src for iframe embeds
This trips people up all the time.
If you are only embedding YouTube with a plain <iframe>, you do not need to allow YouTube in script-src. The iframe is controlled by frame-src, not your page’s script policy.
You only need to touch script-src if your own page loads JavaScript from YouTube or from the YouTube IFrame API.
For a plain embed, this is enough:
Content-Security-Policy:
default-src 'self';
script-src 'self';
frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
object-src 'none';
That’s the version I’d start with.
If you use the YouTube IFrame API
The moment you add this:
<script src="https://www.youtube.com/iframe_api"></script>
your CSP changes. Now your page is loading a remote script, so script-src must allow it.
Example:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://www.youtube.com;
frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
img-src 'self' data: https:;
connect-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
And the page:
<div id="player"></div>
<script src="https://www.youtube.com/iframe_api"></script>
<script>
let player;
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
videoId: 'M7lc1UVf-VE',
playerVars: {
autoplay: 1,
mute: 1,
playsinline: 1
},
events: {
onReady(event) {
event.target.playVideo();
}
}
});
}
</script>
If your CSP blocks inline scripts, which it probably should, that inline block won’t run. Use a nonce or move it into a local JS file.
Nonce example:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-r4nd0m123' https://www.youtube.com;
frame-src 'self' https://www.youtube.com;
object-src 'none';
base-uri 'self';
<div id="player"></div>
<script src="https://www.youtube.com/iframe_api" nonce="r4nd0m123"></script>
<script nonce="r4nd0m123">
let player;
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
videoId: 'M7lc1UVf-VE',
playerVars: { autoplay: 1, mute: 1, playsinline: 1 }
});
}
</script>
If you want a refresher on nonce-based policies and directive behavior, the docs at https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP and the deeper CSP breakdowns at https://csp-guide.com are worth keeping open.
A production-ready policy for a site with YouTube embeds
Here’s a practical baseline I’d use for a content site that embeds YouTube videos but does not use the IFrame API:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
And if you need the API:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://www.youtube.com 'nonce-{{NONCE}}';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
Adapting a real-world CSP
Here’s the real header from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-OTdhMjJjNDgtOWI0ZS00MmU4LWI3MDEtYzY5YmJjZDczMDI5' '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'
Right now, that policy only allows frames from:
'self'https://consentcdn.cookiebot.com
So a YouTube embed will be blocked.
The minimal change is to extend frame-src:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-OTdhMjJjNDgtOWI0ZS00MmU4LWI3MDEtYzY5YmJjZDczMDI5' '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’s enough for standard iframe embeds.
If the site also loads https://www.youtube.com/iframe_api, then script-src needs YouTube too. With 'strict-dynamic', behavior depends on how scripts are bootstrapped and trusted via nonce. In practice, I still prefer being explicit when debugging integrations:
script-src 'self' 'nonce-...' 'strict-dynamic' https://www.youtube.com ...
But for plain iframe embeds, don’t touch script-src. frame-src is the real fix.
Common console errors and what they mean
You’ll usually see something like this:
Refused to frame 'https://www.youtube.com/' because it violates the following Content Security Policy directive: "frame-src 'self' https://consentcdn.cookiebot.com".
That means exactly what it says: add the correct YouTube origin to frame-src.
Another common issue is autoplay not starting with no CSP errors at all. That usually means browser autoplay policy blocked audio playback. Add mute=1.
My recommended setup
For most sites, I’d use:
youtube-nocookie.comif product/legal prefers reduced trackingautoplay=1&mute=1&playsinline=1- a narrow
frame-src - no YouTube in
script-srcunless the IFrame API is actually used
Example final embed:
<iframe
src="https://www.youtube-nocookie.com/embed/M7lc1UVf-VE?autoplay=1&mute=1&playsinline=1"
title="Demo video"
allow="autoplay; encrypted-media; picture-in-picture"
loading="lazy"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen>
</iframe>
Example final CSP:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-src 'self' https://www.youtube-nocookie.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s tight, readable, and easy to maintain. Which is what CSP should be. Not a giant pile of copied origins nobody wants to touch six months later.