Split.io is easy to miss in a CSP until your flags stop loading and your app quietly falls back to defaults.
I’ve seen this happen a lot: the SDK loads fine from your own bundle, but CSP blocks the network calls Split needs for streaming, polling, or event tracking. The result is subtle. No obvious crash, just “why are all my feature flags off in production?”
This guide is the copy-paste version for getting Split.io working with Content Security Policy.
What Split.io usually needs from CSP
For a browser app, Split.io typically needs permission for:
connect-srcfor API callsconnect-srcfor WebSocket streaming if you use realtime updates- sometimes
script-srcif you load Split from a CDN instead of bundling it yourself
Most teams only need to touch connect-src.
If your app bundles the Split SDK with webpack, Vite, Next.js, or similar, you probably do not need to allow any extra third-party script-src host for Split. Your app serves the JavaScript, and Split only shows up as outbound connections.
The minimum CSP change for Split.io
If your app already serves its own JS and only needs Split network access, start here:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://*.split.io wss://*.split.io;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That’s the practical baseline.
Why both schemes?
https://*.split.iocovers normal HTTPS requestswss://*.split.iocovers realtime streaming over WebSocket
If you skip wss:, Split may still work in polling mode, but you lose live updates or get noisy CSP errors.
Copy-paste policy for an existing production CSP
A lot of teams don’t start from a blank policy. They already have a real one, something like this:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MjM1NzhkZTEtNjg3Zi00NjY3LTk3NzEtM2VlNDg5YzlmMzk3' '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 add Split.io support, extend connect-src:
Content-Security-Policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-MjM1NzhkZTEtNjg3Zi00NjY3LTk3NzEtM2VlNDg5YzlmMzk3' '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 https://*.split.io wss://*.split.io; frame-src 'self' https://consentcdn.cookiebot.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
That’s the change I’d make first.
If you load Split from a script tag
I don’t love third-party script tags when I can bundle instead, but sometimes you inherit them.
If Split is loaded directly from a remote script source, you’ll need to allow that host in script-src too. The exact host depends on how you include the SDK, so check the official Split docs and your network panel. A safe pattern is:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://*.split.io;
connect-src 'self' https://*.split.io wss://*.split.io;
img-src 'self' data:;
style-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none'
If your policy uses nonces and 'strict-dynamic', don’t casually bolt on extra hostnames unless you actually need them. In modern setups, a nonce-based policy is usually cleaner than whitelisting lots of script origins.
Recommended policies by setup
1. Split bundled into your app
This is the one I’d choose by default.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://*.split.io wss://*.split.io;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
If you already run a nonce-based setup, Split usually fits in with only the connect-src change.
2. Split with polling only
If you know you are not using streaming, you may be able to avoid WebSocket allowance:
Content-Security-Policy:
default-src 'self';
script-src 'self';
connect-src 'self' https://*.split.io;
object-src 'none';
base-uri 'self';
frame-ancestors 'none'
I’d still test carefully. Some SDK configurations shift behavior over time, and future changes can reintroduce WebSocket use. Allowing both https: and wss: for the same provider is usually the less fragile choice.
3. Report-Only first
This is the smartest way to roll out CSP changes if production is sensitive.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
connect-src 'self' https://*.split.io wss://*.split.io;
report-to csp-endpoint;
report-uri /csp-report
Use this first, watch violations, then move to enforcing mode.
Framework examples
Express / Node.js
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://*.split.io wss://*.split.io",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join("; ")
);
next();
});
Nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://*.split.io wss://*.split.io; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'" always;
Next.js custom headers
module.exports = {
async headers() {
return [
{
source: "/(.*)",
headers: [
{
key: "Content-Security-Policy",
value: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://*.split.io wss://*.split.io; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'"
}
]
}
];
}
};
Common CSP errors with Split.io
Refused to connect to https://...split.io
Your connect-src is missing the required Split host.
Fix:
connect-src 'self' https://*.split.io;
Refused to connect to wss://...split.io
Your app is trying to open a WebSocket for streaming updates.
Fix:
connect-src 'self' https://*.split.io wss://*.split.io;
Refused to load script from a Split host
You are loading Split from a remote script URL and your script-src doesn’t allow it.
Fix either by:
- bundling the SDK locally, which I prefer, or
- adding the exact Split script origin to
script-src
Example:
script-src 'self' https://cdn.split.io;
Only use the real host you actually load from.
Debugging checklist
When Split flags don’t update, I check these in order:
- Browser console for CSP violations
- Network tab for blocked
split.iorequests - WebSocket tab to see whether
wss://connections are attempted - Current SDK loading method: bundled or remote script
- Environment differences between local, staging, and production CSP
The annoying part is that local dev often has no CSP or a very loose one, so everything works until production. That’s why I like testing with a production-like CSP early.
Hardening advice
A few opinions from doing this in real apps:
- Don’t put
https:inconnect-srcjust to “make it work.” That’s lazy and too broad. - Don’t add Split to
default-srcand hope it covers everything neatly. Be explicit withconnect-src. - Don’t keep dead third-party hosts in your policy forever. CSPs rot fast.
- If you use nonces, keep using them. Split usually doesn’t require weakening your script policy.
- Prefer exact origins over wildcards when you know the exact hosts. Use
*.split.ioonly if the SDK genuinely uses multiple subdomains.
Official docs and ready-made examples
For product-specific behavior and current host requirements, check the official Split documentation.
If you want ready-to-use CSP patterns for common app setups, see https://csp-examples.com.
Good default to ship
If you want one practical policy snippet to start with, use this:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://*.split.io wss://*.split.io;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That covers the usual Split.io feature flag setup without blowing a hole in the rest of your CSP.