Feature flags sound harmless until they turn into “run arbitrary code from a remote dashboard.” I’ve seen teams build a custom flag system, then quietly smuggle in dynamic script loading, inline config blobs, third-party SDKs, and admin-controlled HTML. That’s exactly where Content Security Policy starts pulling its weight.
If you’re building a custom flag system, CSP won’t design it for you. What it does is force you to be honest about how the system actually works. Are flags just booleans? Are they JSON payloads? Do they decide which already-shipped component renders? Or are they effectively a remote code execution mechanism with nicer branding?
That distinction matters.
The core question
When people say “CSP for custom flag systems,” they usually mean one of these setups:
- Server-rendered flags embedded into the page
- Client-fetched flags loaded as JSON from an API
- Remote script-based flags loaded from a flag CDN or third-party service
- Inline flag logic injected directly into templates
- Admin-configurable content flags that may include HTML or script-like behavior
These patterns have very different CSP implications.
The safest model: flags as data
The cleanest design is simple:
- ship application code from your origin
- fetch flags as JSON
- let existing code decide what to render
- never execute flag values as code
Example:
<script nonce="{{ .CSPNonce }}">
window.appConfig = {
flagsEndpoint: "/api/flags"
};
</script>
<script nonce="{{ .CSPNonce }}" src="/assets/app.js"></script>
const res = await fetch('/api/flags', { credentials: 'same-origin' });
const flags = await res.json();
if (flags.newCheckout) {
renderNewCheckout();
} else {
renderOldCheckout();
}
This works well with a CSP like:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM}}';
connect-src 'self';
img-src 'self' data:;
style-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
Pros
- Easy to reason about
- Strong CSP is possible
- Flags stay data, not code
- Good fit for SSR and SPA apps
- Limits blast radius if the flag service is compromised
Cons
- Less “flexible” for marketing-driven experiments
- Requires product teams to work within prebuilt UI paths
- You need application code deployed before a flag can activate it
Honestly, those “cons” are mostly good constraints.
The common compromise: server-side inline config
A lot of teams inject a small config object into the HTML because it’s fast and avoids an extra round trip.
<script nonce="{{ .CSPNonce }}">
window.flags = {
checkoutV2: true,
pricingExperiment: "B"
};
</script>
This can still be solid if you use nonces and avoid unsafe patterns.
Pros
- Very fast initial render
- Works well for SSR apps
- No extra request needed for first paint
- Compatible with strict CSP when nonce-based
Cons
- Inline scripts require nonce or hash management
- Templating bugs can become XSS bugs fast
- People get lazy and add
'unsafe-inline'
That last one is where things usually go sideways.
If your app starts with “just one tiny inline script,” six months later someone adds another inline block, then event handlers, then a tag manager exception, and your policy is doing decorative work instead of security work.
The bad idea that keeps coming back: remote script flags
Some flag systems load JavaScript from a remote service and execute it in the page. That might be a vendor SDK, a custom CDN, or some “experimentation engine.”
Example:
<script src="https://flags.example-cdn.com/bootstrap.js"></script>
Then that script decides what else to load or execute.
Pros
- Fast to roll out experiments without app deploys
- Product teams love the control
- Easy vendor integrations
Cons
- Expands your trust boundary dramatically
- A compromise of that remote origin becomes your compromise
- Harder to audit what code actually runs
- Pushes you toward looser
script-src - Makes incident response ugly
If your flag provider can execute arbitrary code in production, you don’t really have a “flag system.” You have delegated script execution.
CSP can reduce some risk here, but not enough to make this model pleasant.
Where strict-dynamic helps — and where it doesn’t
The real-world header from headertest.com is a good example of a modern-ish policy:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-M2NlZmQwN2YtNjU3YS00MTlmLTkyZTQtNTYwMGNiYjU1Y2Yw' '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://collect.tallytics.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'
A couple things stand out:
script-srcuses a nonce- it also uses
strict-dynamic style-srcstill allows'unsafe-inline'- several analytics and consent domains are trusted
For a custom flag system, strict-dynamic is useful if a nonce-approved bootstrap script needs to load additional scripts. That’s better than spraying host allowlists everywhere.
Example:
script-src 'nonce-{{RANDOM}}' 'strict-dynamic';
Then:
<script nonce="{{ .CSPNonce }}">
const s = document.createElement('script');
s.src = '/assets/flags-bootstrap.js';
document.head.appendChild(s);
</script>
Pros of strict-dynamic
- Reduces dependence on brittle host allowlists
- Works well with nonce-based bootstrapping
- Better fit for modern apps with dynamic loading
Cons
- Harder for some teams to understand
- Legacy browser behavior differs
- If your trusted bootstrap script is compromised, CSP won’t save you
That’s the part people miss. CSP is not magic. It formalizes trust. If you nonce a script that does dumb things, you have securely authorized dumb things.
For ready-made patterns around nonces and strict-dynamic, the examples at https://csp-examples.com are useful.
Comparing CSP strategies for custom flag systems
Here’s the blunt version.
1. Flags embedded as JSON in HTML
Best when: SSR app, simple booleans/variants, low latency matters
Recommended CSP shape:
script-src 'self' 'nonce-{{RANDOM}}';
connect-src 'self';
Pros
- Fast
- Reliable
- Easy to lock down
Cons
- Requires nonce plumbing
- Templating must escape safely
2. Flags fetched from same-origin API
Best when: SPA or app with user/session-specific flags
Recommended CSP shape:
script-src 'self';
connect-src 'self';
Pros
- Strongest separation between code and data
- No inline script required if app already boots cleanly
- Easier long-term maintenance
Cons
- Extra request
- Need loading/fallback behavior
This is usually my favorite.
3. Flags fetched from dedicated flag API on another origin
Best when: centralized platform, multi-app architecture
Recommended CSP shape:
script-src 'self';
connect-src 'self' https://flags.example.com;
Pros
- Keeps flags as data
- Supports shared infrastructure
- Still allows strong
script-src
Cons
- Adds another trusted endpoint
- CORS, auth, and outage handling get more complex
Still fine. Just keep it data-only.
4. Remote script execution controlled by flag service
Best when: almost never
Recommended CSP shape: Usually uglier than you want:
script-src 'self' 'nonce-{{RANDOM}}' 'strict-dynamic' https://flags.example-cdn.com;
connect-src 'self' https://api.flags.example.com;
Pros
- Operational flexibility
- Fast experimentation rollout
Cons
- Bigger attack surface
- Harder to audit
- Easier to abuse
- Easier for “flags” to become code injection
I’d avoid this unless there’s a very strong reason and a mature security review process.
What to ban in custom flag systems
If you want CSP to stay meaningful, draw a hard line against these:
Don’t use eval() or new Function()
That forces 'unsafe-eval', which is a big step backward.
// no
const rule = new Function('user', flagRuleString);
If your flag rules need a mini language, parse them safely as data.
Don’t store HTML with script behavior
If admins can define content like this:
<div onclick="launchPromo()">Click me</div>
you’re drifting toward inline-script exceptions and XSS headaches.
Don’t normalize 'unsafe-inline' in script-src
For styles, teams sometimes accept tradeoffs temporarily. For scripts, I’m much less forgiving.
A practical recommendation
If you’re building a custom flag system for a normal product team, I’d use this order of preference:
- Same-origin JSON flags
- Server-rendered JSON flags with nonced bootstrap
- Cross-origin JSON flags via
connect-src - Remote script-based flags only if there’s no sane alternative
A decent starting policy for most custom flag systems looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM}}' 'strict-dynamic';
connect-src 'self' https://flags.example.com;
img-src 'self' data: https:;
style-src 'self';
font-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
If you must embed initial flags inline:
<script nonce="{{ .CSPNonce }}" type="application/json" id="initial-flags">
{"checkoutV2":true,"pricingExperiment":"B"}
</script>
const flags = JSON.parse(
document.getElementById('initial-flags').textContent
);
I like this pattern because it keeps the payload as data, even when it’s in the HTML.
Final take
CSP is great for custom flag systems when the flags are actually flags. Booleans, variants, config, maybe targeting rules represented as data. That’s the sweet spot.
Once your flag platform starts injecting scripts, executable templates, or admin-authored HTML with behavior, CSP becomes a negotiation with bad architecture. You can still patch around it, but you’re spending security budget to preserve a design that probably shouldn’t exist.
If I were reviewing a custom flag system, my first question wouldn’t be “what CSP do we need?” It would be: “Why can this thing execute code at all?”
That answer usually tells you whether the policy will be clean, or whether you’re about to inherit a mess.
For directive details and browser behavior, the official CSP docs are still the baseline reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP.