YouTube embeds look simple right up until your Content Security Policy starts blocking half the player.
I’ve seen this trip up a lot of teams: the page works fine locally, then production sends a strict CSP header and suddenly the iframe is blank, thumbnails don’t load, or the player API silently fails. The fix usually isn’t “disable CSP.” It’s understanding which directives YouTube actually hits, and keeping the allowlist as tight as possible.
The core problem
A YouTube embed is usually an <iframe>, but it doesn’t stop there. The browser may need to load:
- the iframe itself from
youtube.comoryoutube-nocookie.com - images from
i.ytimg.com - scripts inside the iframe, governed by YouTube’s own policy
- optional JS on your page if you use the YouTube Iframe API
- network calls related to playback, tracking, or consent flows
Your CSP only controls your page and what it can load. It does not rewrite YouTube’s internal CSP. That means your main job is:
- allow the iframe to load
- allow any resources your page itself needs around the embed
- avoid opening up broad wildcard policies you don’t need
The minimum CSP for a basic YouTube iframe
If you’re embedding plain HTML like this:
<iframe
width="560"
height="315"
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen>
</iframe>
The key directive is frame-src:
Content-Security-Policy: default-src 'self'; frame-src https://www.youtube.com;
That’s enough for the iframe document itself.
If you use YouTube privacy-enhanced mode, the iframe host changes:
<iframe
src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
title="YouTube video player"
allowfullscreen>
</iframe>
Then your CSP should allow that host instead:
Content-Security-Policy: default-src 'self'; frame-src https://www.youtube-nocookie.com;
If your site uses both embed formats, allow both:
Content-Security-Policy:
default-src 'self';
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
Why child-src sometimes shows up
Older CSP examples use child-src for frames. Modern policies should prefer frame-src for iframes specifically. If you want the deeper background on directive behavior and fallback rules, csp-guide.com is a solid reference.
For modern browser support, I’d write:
Content-Security-Policy:
default-src 'self';
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
Not this:
Content-Security-Policy:
default-src 'self';
child-src https://www.youtube.com;
The policy most teams actually need
A basic embed often also needs thumbnails or preview artwork. Those usually come from i.ytimg.com.
So a more realistic CSP looks like this:
Content-Security-Policy:
default-src 'self';
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
img-src 'self' https://i.ytimg.com;
If your page has no inline scripts and no player API, that may be enough.
If you use the YouTube Iframe API
A lot of developers don’t just embed the iframe; they control playback with JavaScript.
Example:
<div id="player"></div>
<script src="https://www.youtube.com/iframe_api"></script>
<script nonce="{{ .CSPNonce }}">
let player;
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
videoId: 'dQw4w9WgXcQ',
playerVars: {
playsinline: 1
}
});
}
</script>
Now your CSP needs to allow:
- the external script from
youtube.com - your inline script via nonce or hash
- the frame host used by the player
Example:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0m123' https://www.youtube.com;
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
img-src 'self' https://i.ytimg.com;
If you want to be stricter, avoid 'unsafe-inline' and use nonces or hashes for the inline bootstrap code. That’s the sane choice on any modern app.
A practical Express example
Here’s a Node/Express setup using helmet:
import express from "express";
import helmet from "helmet";
import crypto from "crypto";
const app = express();
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString("base64");
next();
});
app.use((req, res, next) => {
helmet({
contentSecurityPolicy: {
useDefaults: true,
directives: {
"default-src": ["'self'"],
"script-src": [
"'self'",
(req, res) => `'nonce-${res.locals.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"
],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"frame-ancestors": ["'none'"]
}
}
})(req, res, next);
});
app.get("/", (req, res) => {
res.send(`
<!doctype html>
<html>
<head>
<title>YouTube CSP Demo</title>
</head>
<body>
<div id="player"></div>
<script nonce="${res.locals.nonce}" src="https://www.youtube.com/iframe_api"></script>
<script nonce="${res.locals.nonce}">
let player;
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
videoId: 'dQw4w9WgXcQ'
});
}
</script>
</body>
</html>
`);
});
app.listen(3000);
That’s a decent baseline. Tight enough to be useful, not so strict that the player breaks instantly.
Don’t confuse frame-src and frame-ancestors
This one causes constant confusion:
frame-srccontrols what your page can embedframe-ancestorscontrols who can embed your page
If you want to allow YouTube embeds on your page, frame-src is the directive that matters.
If you don’t want anyone framing your own site, use:
frame-ancestors 'none';
That’s exactly what the real CSP header on headertest.com does:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YjI3ZGQzOWQtNmMxNy00N2UxLWE0MzQtOTM1NzVhN2NiYWM5' '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://u.headertest.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'
That policy allows a specific framed resource with frame-src, while separately blocking the site itself from being framed using frame-ancestors 'none'.
Common breakages with YouTube embeds
1. You allow youtube.com but use youtube-nocookie.com
This is the classic one.
If your embed URL is:
src="https://www.youtube-nocookie.com/embed/..."
then this policy will fail:
frame-src https://www.youtube.com;
You need:
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
2. Thumbnail images are blocked
If you render custom preview cards like this:
<img src="https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg" alt="Video preview">
and forget img-src, the image won’t load.
Fix:
img-src 'self' https://i.ytimg.com;
3. The API script is blocked
If you load:
<script src="https://www.youtube.com/iframe_api"></script>
but your script-src only allows 'self', the player API won’t initialize.
Fix:
script-src 'self' https://www.youtube.com;
Or, if you also have inline setup code, use a nonce:
script-src 'self' 'nonce-abc123' https://www.youtube.com;
4. You rely on default-src and forget explicit directives
A lot of people write:
default-src 'self';
and assume that’s enough. It isn’t. frame-src, img-src, and script-src often need explicit values for YouTube-related resources.
A stricter production-ready example
If I were shipping a page with YouTube embeds and no extra third-party junk, I’d start with something like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{NONCE}}' https://www.youtube.com;
style-src 'self';
img-src 'self' data: https://i.ytimg.com;
frame-src 'self' https://www.youtube.com https://www.youtube-nocookie.com;
connect-src 'self';
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
A few opinions here:
- I would not add
https:toimg-srcjust because it makes the errors go away. - I would not use wildcard hosts unless I had verified they were actually needed.
- I would not add
'unsafe-inline'toscript-srcfor a single embed bootstrap snippet.
That’s how CSP turns into theater instead of protection.
Debugging blocked embeds
When YouTube breaks under CSP, check these in order:
-
Browser DevTools Console
CSP violations are usually explicit. -
Network tab
See which URL got blocked. -
Your actual response headers
Make sure the deployed header matches what you think you sent. -
Report-Only mode
Great for testing before enforcement.
Example:
Content-Security-Policy-Report-Only:
default-src 'self';
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
img-src 'self' https://i.ytimg.com;
report-to default-endpoint;
If you’re troubleshooting a live policy, always inspect the final header delivered to the browser. Reverse proxies, CDNs, and framework middleware love to “help” in ways that break CSP.
When to use youtube-nocookie.com
If you care about reducing tracking before user interaction, use the privacy-enhanced embed domain:
src="https://www.youtube-nocookie.com/embed/dQw4w9WgXcQ"
That doesn’t magically eliminate all privacy concerns, but it’s generally the better default than standard youtube.com embeds. Your CSP just needs to reflect that host.
The short version
For plain YouTube iframes:
Content-Security-Policy:
default-src 'self';
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
img-src 'self' https://i.ytimg.com;
For embeds plus the YouTube Iframe API:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{NONCE}}' https://www.youtube.com;
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
img-src 'self' https://i.ytimg.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
That covers most real-world setups without turning your policy into a giant allowlist mess. The trick is staying specific: allow the exact frame host, exact script host, and exact image host you actually use. That’s the difference between a CSP that protects something and one that just looks impressive in a security review.