CSP for Iframely Embeds Without Breaking Your Site
Table of Contents
Iframely is one of those tools that looks simple until CSP gets involved. Paste a URL, get a rich embed, ship it. Then you turn on a strict Content Security Policy and half your embeds stop rendering, thumbnails vanish, or a provider silently fails inside an iframe.
I’ve dealt with this a few times, and the tricky part is that “Iframely embed” can mean different things:
- A plain iframe hosted by Iframely
- A script-based embed that injects markup
- An Iframely result that itself loads third-party content from YouTube, X, Vimeo, Spotify, and friends
That means your CSP has to account for both Iframely and whatever the final provider needs.
The core CSP directives that matter
For Iframely, these directives usually decide whether things work:
script-src: if you load Iframely’s embed scriptframe-src: if the embed renders as an iframeimg-src: thumbnails and preview imagesmedia-src: audio/video assets in some providersconnect-src: API calls from script-based embedsstyle-src: inline or remote styles used by the embedchild-src: older fallback for frames in legacy setups
If your site uses a locked-down policy like this 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-ZmFhN2E2OTItNmE5MC00NTZiLThkMjMtMjE3ZTU1MzBhY2Jm' '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'
Iframely won’t work out of the box. There’s no allowance for Iframely scripts, frames, or API endpoints.
If you want to inspect your live CSP and spot breakage fast, HeaderTest is handy for seeing the policy exactly as browsers receive it.
Start with the embed type
1. Iframe embed
A typical iframe embed looks like this:
<iframe
src="https://cdn.iframe.ly/api/iframe?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dabc123&key=YOUR_KEY"
width="640"
height="360"
loading="lazy"
allowfullscreen>
</iframe>
For this style, the minimum CSP change is usually frame-src.
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://cdn.iframe.ly;
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
That allows the iframe itself, but not necessarily everything inside it if the final provider gets loaded directly from another origin. Some providers stay inside the Iframely-hosted frame, others may involve redirects or nested frames. In practice, you may need:
frame-src 'self' https://cdn.iframe.ly https://*.youtube.com https://player.vimeo.com https://open.spotify.com;
That’s why I don’t recommend “just add Iframely and done.” Test actual URLs your product supports.
2. Script embed
Another common pattern loads Iframely’s JavaScript:
<a
data-iframely-url="//iframely.net/abcdef?card=small"
href="https://example.com/some-content">
</a>
<script async src="//cdn.iframe.ly/embed.js"></script>
Now your CSP needs script-src, and often frame-src, img-src, and connect-src too.
A baseline policy for that looks like:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.iframe.ly;
connect-src 'self' https://cdn.iframe.ly https://iframely.net;
frame-src 'self' https://cdn.iframe.ly https://iframely.net;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
The ugly bit here is style-src 'unsafe-inline'. Some embed libraries inject inline styles. If you can avoid that with your exact integration, do it. If not, you’re trading some strictness for functionality.
Adapting a strict production CSP
Say your current policy is close to the HeaderTest example. Here’s a realistic adjusted version for Iframely script embeds:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-ZmFhN2E2OTItNmE5MC00NTZiLThkMjMtMjE3ZTU1MzBhY2Jm' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://cdn.iframe.ly;
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 https://cdn.iframe.ly https://iframely.net;
frame-src 'self' https://consentcdn.cookiebot.com https://cdn.iframe.ly https://iframely.net;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s still probably incomplete for real-world embeds. If users can paste YouTube, TikTok, or Vimeo links, expect provider-specific additions.
The provider problem
Iframely is a proxy and formatter, but embedded media often still comes from the original service. That means a YouTube card may need:
frame-src https://www.youtube.com https://www.youtube-nocookie.com;
img-src https://i.ytimg.com;
A Vimeo embed may need:
frame-src https://player.vimeo.com;
img-src https://i.vimeocdn.com;
Spotify:
frame-src https://open.spotify.com;
This is where teams usually go wrong: they write one policy for Iframely’s domain and forget the downstream providers.
If you need starter policies for common embed platforms, csp-examples.com is a good place to grab patterns and then trim them down.
A practical allowlist strategy
Don’t try to support every provider on day one. That turns into https: everywhere, which defeats the point of CSP.
I prefer this approach:
- List the providers your app actually allows
- Add CSP entries only for those providers
- Log violations in report-only mode first
- Promote to enforcing once you’ve tested real content
Example: if your product only supports YouTube, Vimeo, and Spotify through Iframely:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.iframe.ly;
connect-src 'self' https://cdn.iframe.ly https://iframely.net;
frame-src 'self' https://cdn.iframe.ly https://iframely.net https://www.youtube.com https://www.youtube-nocookie.com https://player.vimeo.com https://open.spotify.com;
img-src 'self' data: https://i.ytimg.com https://i.vimeocdn.com https://cdn.iframe.ly;
style-src 'self' 'unsafe-inline';
media-src 'self' https:;
object-src 'none';
base-uri 'self';
form-action 'self';
Not perfect, but much better than frame-src https:.
Use report-only before enforcing
This saves a lot of pain.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.iframe.ly;
connect-src 'self' https://cdn.iframe.ly https://iframely.net;
frame-src 'self' https://cdn.iframe.ly https://iframely.net;
img-src 'self' data: https:;
report-uri /csp-report;
Then collect reports server-side.
Example Node/Express endpoint:
import express from "express";
const app = express();
app.use(express.json({ type: ["application/json", "application/csp-report"] }));
app.post("/csp-report", (req, res) => {
console.log("CSP violation:", JSON.stringify(req.body, null, 2));
res.sendStatus(204);
});
app.listen(3000);
A typical violation for a blocked embed might look like:
{
"csp-report": {
"document-uri": "https://app.example.com/post/123",
"violated-directive": "frame-src",
"blocked-uri": "https://player.vimeo.com"
}
}
That tells you exactly what to add.
Framework examples
Nginx
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' https://cdn.iframe.ly;
connect-src 'self' https://cdn.iframe.ly https://iframely.net;
frame-src 'self' https://cdn.iframe.ly https://iframely.net https://www.youtube.com https://player.vimeo.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
" always;
Express with Helmet
import express from "express";
import helmet from "helmet";
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://cdn.iframe.ly"],
connectSrc: ["'self'", "https://cdn.iframe.ly", "https://iframely.net"],
frameSrc: [
"'self'",
"https://cdn.iframe.ly",
"https://iframely.net",
"https://www.youtube.com",
"https://player.vimeo.com"
],
imgSrc: ["'self'", "data:", "https:"],
styleSrc: ["'self'", "'unsafe-inline'"],
objectSrc: ["'none'"],
baseUri: ["'self'"]
}
}
})
);
app.listen(3000);
Common mistakes
Forgetting frame-src
This is the big one. The iframe tag exists, but the browser refuses to load it.
Allowing only Iframely, not the provider
Works for some URLs, fails for others.
Using default-src and assuming it covers everything well
It does act as a fallback, but explicit directives like script-src, frame-src, and connect-src are where real control happens.
Going straight to https:
Yes, it fixes the problem. It also weakens your policy enough that you’ll regret it later.
Ignoring frame-ancestors
This one doesn’t affect loading Iframely embeds on your page, but it does control whether your site can be framed by others. Keep it intentional.
My recommended baseline
If you’re adding Iframely to a developer product and want a sane starting point, I’d begin here and then tighten per provider:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.iframe.ly;
connect-src 'self' https://cdn.iframe.ly https://iframely.net;
frame-src 'self' https://cdn.iframe.ly https://iframely.net;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
media-src 'self' https:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Then watch report-only violations and add only the providers you actually support.
That’s the whole game with CSP and Iframely: start narrow, test real embeds, and resist the temptation to paper over everything with giant wildcards.