SoundCloud embeds look simple: paste an <iframe>, ship it, done. Then CSP gets involved and the player disappears, the console fills with violations, and somebody “fixes” it by slapping https: into half the policy.
That’s the usual failure mode.
If you’re embedding SoundCloud on a site with a real Content Security Policy, the trick is to allow exactly what the embed needs and nothing else. Most breakages come from guessing the wrong directive, overusing default-src, or trying to force a third-party widget into a policy designed only for first-party code.
Here are the mistakes I see most often and how I’d fix them.
Mistake #1: Allowing SoundCloud in script-src instead of frame-src
This is the most common one.
A SoundCloud embed is typically an iframe like this:
<iframe
width="100%"
height="166"
scrolling="no"
frameborder="no"
allow="autoplay"
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/123456789">
</iframe>
That means the browser loads content from w.soundcloud.com as a frame, not as a script running in your page context.
If your CSP only does this:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://w.soundcloud.com;
it still won’t work. The browser checks frame-src for iframe loads.
Fix
Allow SoundCloud in frame-src:
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://w.soundcloud.com;
If you’re relying on default-src fallback, technically default-src can apply when frame-src is absent, but I don’t recommend leaning on that. Be explicit. If a policy has to support real production changes over time, explicit directives are easier to reason about and debug.
If you want a refresher on directive behavior and fallback rules, https://csp-guide.com is a good quick reference.
Mistake #2: Forgetting that your existing frame-src overrides default-src
This one bites teams that already have a mature CSP.
Here’s a 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-MDRiYjBmMGEtYTlhOC00YjExLThlMjUtOWJkZjIxMTBhYTk4' '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'
At first glance, some people look at default-src and think, “I’ll just add SoundCloud there.” But this policy already defines:
frame-src 'self' https://consentcdn.cookiebot.com;
Once frame-src exists, the browser uses that for frames. default-src no longer matters for iframe loading.
Fix
Add SoundCloud to frame-src, not just default-src:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-...' '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://w.soundcloud.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s the minimum change I’d make first.
Mistake #3: Whitelisting all of soundcloud.com when the embed only needs w.soundcloud.com
Developers love broad allowlists because they end the immediate pain. Security people hate them because they become permanent.
For iframe embeds, the usual host you need is:
https://w.soundcloud.com
I see policies like this all the time:
frame-src 'self' https://*.soundcloud.com;
That works, but it’s sloppy. If you know the player is served from w.soundcloud.com, allow that host and move on.
Fix
Prefer the narrow origin:
frame-src 'self' https://w.soundcloud.com;
If your implementation later shows actual blocked requests to another SoundCloud subdomain, add it based on evidence, not vibes.
That’s my default CSP rule for third-party services: start narrow, expand only from observed violations.
Mistake #4: Assuming the iframe can do whatever it wants without affecting your CSP
This is where things get subtle.
The framed document has its own origin and its own CSP context, so your script-src does not govern scripts running inside the SoundCloud player. That part is true.
But your page still controls whether the browser may load the frame at all via frame-src. And depending on how you integrate the widget, your page may also trigger related requests for thumbnails, preload assets, or wrapper scripts.
For example, if your page also renders a SoundCloud artwork URL outside the iframe:
<img src="https://i1.sndcdn.com/artworks-abc123-t500x500.jpg" alt="Track artwork">
then img-src needs to allow that host. The iframe allowance alone won’t help.
Fix
Keep the embed requirements separate from your own page’s requirements.
For a plain iframe-only integration, start here:
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://w.soundcloud.com;
object-src 'none';
base-uri 'self';
If your page itself loads SoundCloud-hosted images or calls SoundCloud APIs, add those to img-src or connect-src as needed after checking actual blocked URLs.
Don’t preemptively dump https://*.soundcloud.com into every fetch directive. That’s how CSP turns into decorative text.
Mistake #5: Breaking embeds with an overly aggressive sandbox attribute
This isn’t strictly CSP, but it shows up alongside CSP hardening all the time.
People add an iframe sandbox like this:
<iframe
sandbox
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/123456789">
</iframe>
That heavily restricts the frame. In practice, it often breaks player functionality.
Then they blame CSP because the result looks the same: no working player.
Fix
If you use sandbox, be deliberate about permissions. For example:
<iframe
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
allow="autoplay"
src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/123456789">
</iframe>
Whether you need all of those depends on your use case. I’d test with the smallest set that preserves the player behavior you actually want.
If the player opens links, pops out, or needs script execution, stripping sandbox permissions too far will break it no matter how perfect your CSP is.
Mistake #6: Using media-src for the iframe and wondering why nothing changed
I’ve seen this more than once:
media-src https://w.soundcloud.com;
That won’t allow the embed iframe. media-src is for things like <audio> and <video> resource loads, not framed documents.
If you’re not directly embedding audio files with <audio src="...">, media-src may not matter at all for the basic SoundCloud widget.
Fix
Use the right directive for the right resource type:
frame-srcfor the SoundCloud iframeimg-srcfor artwork loaded by your pageconnect-srcfor API/XHR/fetch/WebSocket calls from your pagemedia-srconly if your page directly loads media resources
A minimal working policy for the embed usually looks like this:
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://w.soundcloud.com;
object-src 'none';
base-uri 'self';
form-action 'self';
Mistake #7: Debugging from the policy instead of the browser console
People love editing CSP by intuition. Bad habit.
If SoundCloud is blocked, the browser will usually tell you exactly which directive blocked which URL. Read that message first.
Typical examples:
Refused to frame 'https://w.soundcloud.com/' because it violates the following Content Security Policy directive: "frame-src 'self'".
Or:
Refused to load the image 'https://i1.sndcdn.com/...' because it violates the following Content Security Policy directive: "img-src 'self' data:".
Those are not the same problem. One needs frame-src; the other needs img-src.
Fix
Use a report-only rollout before enforcing changes:
Content-Security-Policy-Report-Only:
default-src 'self';
frame-src 'self' https://w.soundcloud.com;
report-to default-endpoint;
That lets you test the policy without breaking production traffic immediately. If you already have a mature CSP, this is the safest way to add third-party embeds.
For the actual syntax and reporting behavior, the official CSP docs are still the source I trust most: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP.
A sane baseline policy for SoundCloud embeds
If I were adding a standard SoundCloud iframe to a typical app, I’d start here:
Content-Security-Policy:
default-src 'self';
frame-src 'self' https://w.soundcloud.com;
img-src 'self' data: https:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
And if I were patching the headertest.com-style policy shown earlier, I’d make the smallest possible change:
frame-src 'self' https://consentcdn.cookiebot.com https://w.soundcloud.com;
That’s it unless the console proves you need more.
My rule of thumb
For SoundCloud embeds, don’t treat CSP like a giant domain allowlist. Treat it like a resource map.
Ask:
- Is this an iframe? Use
frame-src. - Is my page loading images from SoundCloud? Use
img-src. - Is my code making API requests? Use
connect-src. - Did I actually observe a blocked request, or am I guessing?
Most CSP mistakes happen when people skip step four.
If you keep the policy narrow and make changes from real violations, SoundCloud embeds are pretty easy to support without punching a giant hole in your CSP.