Writing
Securityregulated

Server-driven UI breaks under a strict CSP — here’s the proof

If your UI is generated from strings at runtime, a no-unsafe-eval Content-Security-Policy turns it off. Why code-from-strings and a strict CSP are structurally at odds — with a running demo and the test that proves it.

Server-driven UI has an easy promise: describe a screen on the server, ship it to any client, change it without a deploy. The trouble shows up the first time that description needs to do something — a visibility rule, a computed label, a conditional. The fast way to add behavior to a JSON payload is to evaluate a little expression language at runtime. And runtime expression evaluation is exactly what a strict Content-Security-Policy exists to forbid.

The failure, reproduced

You don’t have to theorize about this. A Builder.io user shipped a page whose data bindings worked perfectly in the editor, deployed it behind a platform with a strict policy, and got this:

console — production build
// data binding that renders fine in the editor…
Failed code evaluation: Code generation from strings
disallowed for this context

The binding was compiled from a string at runtime — the platform ran the moral equivalent of new Function("return state.username") to resolve it. That’s the reported issue, closed by the vendor in March 2025, and it is not a one-off: any engine that turns strings into executable code hits the same wall the moment the runtime refuses to generate code from strings.

Why a strict CSP turns it off

A Content-Security-Policy without 'unsafe-eval' disables the whole family of string-to-code APIs: eval(), the Function constructor, and setTimeout("…") with a string body. That is the recommended posture for any page that handles sensitive input.

response header
Content-Security-Policy: script-src 'self'
// no 'unsafe-eval' → eval() and new Function() throw

So this isn’t a bug you patch. Code-from-strings and a no-unsafe-eval policy are structurally at odds. If your UI layer needs one, it cannot run under the other — and the pages where the policy is strictest (checkout, onboarding, anything touching account data) are precisely the ones you’d most want to drive from the server.

The proof: watch it refuse

The way out is to draw a hard line: the contract describes structure, not behavior. It carries no expression language, so there is nothing to evaluate — real logic stays in typed, reviewable code on your side, never in the payload. That claim is only worth anything if you can check it, so we do, in a test that swaps eval and the Function constructor for traps and validates a real contract:

blueprint-core/src/no-eval.test.ts
it('validates a contract without ever invoking new Function or eval', () => {
  globalThis.Function = () => { throw new Error('new Function was called') }
  // eval trapped the same way
  validate(contract)  // passes — nothing is generated from strings
})

And it isn’t just a unit test asserting an absence — the whole thing renders live under a strict CSP, with the policy visible in the response headers, on the demo page. Watch it run under a strict CSP

The honest part

This doesn’t prove every server-driven-UI tool evaluates strings — some don’t. It proves something narrower and more useful: any tool that does cannot pass a strict CSP, and you can’t always tell which camp a tool is in until you deploy. The only durable guarantee is a contract with no expression language to evaluate in the first place — and a test that fails the build if one ever sneaks in.

Why this lands hardest on a payment page

Since 31 March 2025, PCI DSS 4.0 requires every script on a payment page to be inventoried, justified, and monitored for tampering (§6.4.3 and §11.6.1). A UI layer that generates code from strings at runtime is the opposite of that: code that didn’t exist when the page was reviewed, materializing in the browser. A contract that ships zero eval — and whose every change is a reviewable diff — turns that standing finding into a non-issue. See the regulated case

Server-driven UI is worth doing. Shipping an interpreter to the browser to get it is not. Keep the contract describing structure, keep the behavior in code you review, and a strict CSP has nothing left to reject.