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:

  1. Server-rendered flags embedded into the page
  2. Client-fetched flags loaded as JSON from an API
  3. Remote script-based flags loaded from a flag CDN or third-party service
  4. Inline flag logic injected directly into templates
  5. 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-src uses a nonce
  • it also uses strict-dynamic
  • style-src still 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:

  1. Same-origin JSON flags
  2. Server-rendered JSON flags with nonced bootstrap
  3. Cross-origin JSON flags via connect-src
  4. 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.