Notes · Dissecting Real Systems

growing

Can I Use This Library?

A strict CSP quietly turns every dependency into a security decision. Here is the tree I walk to make it — per library, and across a whole app.

· · 14 min read

security, csp, web, architecture, dependencies, dissecting-systems

You can't trust code that you did not totally create yourself. (Especially code from companies that employ people like me.)

— Ken Thompson, "Reflections on Trusting Trust", Turing Award lecture, CACM 27(8), 1984

Cite this
APA
Mangalapilly, Y. J. (2026, July). Can I Use This Library?. Saṃhitā Notes. https://yesudeep.com/blog/can-i-use-this-library/
BibTeX
@online{mangalapilly2026can,
          author  = {Yesudeep Jose Mangalapilly},
          title   = {Can I Use This Library?},
          journal = {Sa\d{m}hit\=a Notes},
          year    = {2026},
          month   = {July},
          url     = {https://yesudeep.com/blog/can-i-use-this-library/},
          urldate = {2026-07-03},
        }
Plain
Yesudeep Jose Mangalapilly. “Can I Use This Library?.” Saṃhitā Notes, 2026. https://yesudeep.com/blog/can-i-use-this-library/.
RIS
TY  - ELEC
        AU  - Mangalapilly, Yesudeep Jose
        TI  - Can I Use This Library?
        T2  - Saṃhitā Notes
        PY  - 2026
        UR  - https://yesudeep.com/blog/can-i-use-this-library/
        Y2  - 2026-07-03
        ER  - 

How a strict CSP reshapes which libraries you can adopt — and a decision tree for making the call. By the end you'll have a four-question test to run against any front-end dependency, the four remedies when it fails (reject, sandbox, wrap, self-host — with a clean adopt when all four gates pass), and a way to apply the same logic to a whole codebase rather than one package at a time.

In the strict-CSP piece I noted, almost in passing, that the policy "quietly reshapes which libraries you let developers adopt at all." That sentence deserves its own page, because it's the part of CSP work that surprises engineering leaders most. A strict CSP isn't only a header you set once; it's a standing constraint on your dependency graph. Every npm install becomes, in a small way, a security review.

Under a strict CSP, a dependency that runs eval, injects its own inline scripts, or writes raw innerHTML is no longer a free choice. The header doesn't care that it's popular — it cares what it does at runtime.

Why a library can break a policy

Recall what a strict CSP forbids. With script-src 'nonce-…' 'strict-dynamic' and require-trusted-types-for 'script', the browser will run only scripts your server vouched for (by nonce or hash), refuse eval and new Function unless you add unsafe-eval (which guts the policy), and reject any string that reaches a DOM-injection sink like innerHTML without passing through a typed policy.

A library you pull in runs in your origin, with your policy. If it does any of those forbidden things internally, it doesn't fail quietly at build time — it throws at runtime, in production, in the one code path you didn't test. So the question "can I use this library?" stops being about license and bundle size and becomes, also, "does it do anything my policy forbids?"

Think of your strict CSP as a clean-room lab: everything that enters has to come through the airlock you control. A normal library is a sealed sample — fine. But some libraries insist on making new material inside the room (eval), or bringing in their own unvetted samples (inline scripts), or writing on the walls directly (innerHTML). Those don't get to just walk in. You either find one that respects the airlock, put it behind glass, or turn it away.

The tree, one dependency at a time

Here's the test I actually run. It's four questions, each about runtime behavior, and a "yes" to any of them sends you to a remedy rather than a flat no — because most libraries are salvageable.

The per-dependency test. Walk the gates top to bottom; a "yes" at any one routes you to a remedy (reject, sandbox, wrap, self-host). Only a dependency that answers "no" all the way down is a clean adopt.

Gate 1 — does it eval or new Function?

This is the disqualifier. Dynamic code generation — eval(), new Function(), setTimeout("…string…") — is exactly what a strict CSP exists to stop, and the only way to allow it is unsafe-eval, which re-opens the hole for everything, not just this library. The one narrow exception is WebAssembly: CSP3's 'wasm-unsafe-eval' permits WASM compilation while — in the spec's words — it "only permits WebAssembly and does not affect JavaScript," so a WASM-heavy library (sql.js, ffmpeg.wasm) needs that keyword, not the full unsafe-eval surrender. For JavaScript eval there is no narrow exception.

A few well-known offenders historically eval'd: older template engines that compiled templates to functions, some date/expression parsers, and a number of charting and math libraries. Many have since shipped "CSP-safe" or "no-eval" builds precisely because of this pressure — always check for one before rejecting.

Remedy: reject — or find the eval-free fork or build flag, which many popular libraries now ship specifically because strict CSP forced the issue. If neither exists, this dependency is incompatible, full stop.

Gate 2 — does it inject its own inline scripts?

Some libraries (especially analytics, tag managers, A/B-testing tools, and embedded widgets) work by writing a <script> element into the page at runtime. Under strict-dynamic that can be made to work — if the injecting script is itself trusted, the scripts it adds inherit the trust — but only for non-parser-inserted insertion: per the CSP3 spec, a script created with createElement and appended inherits trust, while "document.write() produces script elements which are 'parser-inserted'" and gets no trust. Since legacy widgets and ad tags document.write their loaders as a matter of habit, this caveat is the actual triage question for this gate — and third-party widgets also frequently inject scripts from hosts you never audited.

Remedy: sandbox — put the widget in an <iframe sandbox> (the sandbox attribute) served from a separate origin with its own looser policy, so its mess is contained to a frame that can't touch yours. The separate origin matters: a same-origin srcdoc or blob: frame inherits the embedder's CSP (CSP3 §7.8 — "documents loaded from local schemes will inherit a copy of the policies"), so sandboxing in place relaxes nothing. This is the standard move for ad tags, chat widgets, and anything you don't control end-to-end.

Gate 3 — does it write raw innerHTML (or equivalent)?

This is the most common failure, and the most fixable. A library that builds DOM by assigning strings to innerHTML, outerHTML, insertAdjacentHTML, or document.write will trip Trusted Types the moment you turn it on. The catch is that this is also the most common XSS sink, so the policy is right to stop it.

Remedy: wrap — route the library's sink calls through a Trusted Types policy that sanitizes (with something like DOMPurify) before the string reaches the DOM. Many libraries now accept a custom sanitizer or a trustedTypes hook for exactly this. Wrapping turns an unsafe sink into a vetted one without forking the library.

Gate 4 — does it require an allowlisted host?

A library delivered only from a third-party CDN, or one that loads its own sub-resources from fixed hosts, invites you to add those hosts to script-src. Be precise about what does and doesn't break here: nothing stops you from stamping your nonce on a cross-origin <script src> — a nonce vouches for the element wherever its source lives, and under strict-dynamic the host allowlist is simply ignored. The problem is what that nonce means. You have granted a script you don't build, on a server you don't control, full execution and — under strict-dynamic — the right to load whatever it likes next; when the CDN ships a compromised version tomorrow, your policy vouches for it sight unseen. And SRI can't pin an auto-updating CDN URL, so the usual mitigation doesn't compose.

Remedy: self-host — vendor the library into your own build, where it gets a nonce or hash like everything else, updates when you update it, and adds no third-party server to your trust base. If self-hosting isn't possible, weigh whether you need it at all.

All four "no" — adopt

A dependency that's self-hosted, generates no code at runtime, injects no inline scripts, and touches the DOM through safe APIs (or accepts a Trusted Types hook) is a clean adopt. It will work under the strict policy with no special handling — which, not coincidentally, is also what a well-engineered modern library looks like.

The tree has a pleasant side effect: the libraries that pass it are, almost exactly, the ones you'd want anyway. "CSP-safe" turns out to be a proxy for "doesn't do anything alarming at runtime."

The tree, applied: real libraries and frameworks

The gates are abstract until you run them against things you actually ship. So here is the test applied to a representative set, scored against the four questions — each verdict checked against the project's own docs and issue tracker, not folklore. The surprises are mostly pleasant: several libraries "everyone knows" need unsafe-eval don't, in the build you actually use.

First, the frameworks:

Five front-end frameworks under a strict CSP. "eval in prod?" is the disqualifier; "nonce wiring" is whether you must thread a nonce through; "Trusted Types" is innerHTML-sink handling. The "needs unsafe-eval" reputation attaches to the wrong build in three of these — see the "eval in prod?" column.
framework eval in prod? nonce wiring Trusted Types verdict
React 18.3+ no (dev only) SSR nonce option off by default \to wrap nonce + wrap
Vue 3 no (runtime-only build) via Vite html.cspNonce auto since 3.5 adopt + light wiring
Angular no (AOT, default) ngCspNonce (v16+) first-class (v11+) nonce wiring
SvelteKit no kit.csp (auto) fixed in Svelte 5.52 nonce wiring
Next.js no* auto (App Router) partial nonce + wrap

The asterisk on Next.js: an open issue (vercel/next.js#81496) reports a bundled util polyfill calling Function() in production if your code reaches util.promisify. "No eval in prod" is the official position; verify that path before you trust it.

The headline is the eval in prod? column. "Vue needs unsafe-eval" is true only of the full, in-browser-compiler build; the runtime-only build every Vite project uses precompiles templates and is CSP-clean. "Angular needs unsafe-eval" conflates the JIT compiler (dev) with AOT (the default since v9), which precompiles and doesn't. "React needs unsafe-eval in production" is simply false — its eval is dev-only. Three reputations, all attached to a build you don't ship.

The catch shared by all five is the second column: a nonce-based CSP means wiring a fresh nonce through the framework's own inline scripts — the uncacheable side of the policy. Angular and SvelteKit document this best; React and Next.js make you assemble it; all of them now support it.

Now the libraries — where the picture is more varied, because a library's job description often is one of the forbidden things:

Eleven common libraries by their runtime behavior. The eval column names exactly what evals (almost always one isolable feature, not the whole library); "DOM sink" is innerHTML-style writing. Lit and DOMPurify are the standouts — both clean by design, with Lit auto-creating its own Trusted Types policy and DOMPurify being the wrapper the others' "wrap" remedy points to.
library runtime eval? DOM sink / TT verdict
Lit (2 / 3) no (static templates) auto TT policy lit-html adopt (cleanest here)
Handlebars yes — runtime compile string out, no TT adopt: precompile + wrap
Lodash only _.template none adopt (avoid/precompile _.template)
Chart.js (v3+) no (since v2.0) canvas, CSP-safe adopt
DOMPurify no emits TrustedHTML adopt — this is the wrapper
D3.js only d3-dsv.parse .html() sinks, no TT adopt + wrap; use parseRows
Alpine.js yes (new Function) x-html, no TT reject, or @alpinejs/csp build
htmx gated (allowEval) swap sink, no TT wrap: allowEval:false + TT policy
jQuery (4.0+) script-tag AJAX (nonce via scriptAttrs) TT input pass-through wrap; feed TrustedHTML, pin 4.0\ge 4.0
Vue 2 (full) yes (new Function) v-html, no TT reject; runtime-only build is safe
AngularJS 1.x yes (ng-csp disables) no TT ($sce) reject — EOL + bypass gadget

The eval in most of these is isolable. Lodash only generates code in _.template; D3 only in d3-dsv's parse (use parseRows instead); Handlebars only when compiling templates at runtime (precompile at build). "This library needs unsafe-eval" almost always means "one feature does" — and that feature usually has an avoidable alternative.

A few rows are worth reading closely. Lit is the most instructive, because it shows what "CSP-safe by design" actually looks like. A templating library is exactly the kind of thing you'd expect to need eval — Alpine and the full builds of Vue 2 and AngularJS all generate functions from template strings at runtime. Lit doesn't, and the reason is architectural: its templates are written as tagged template literals, so the static HTML is fixed at authoring time and there's simply nothing to compile at runtime — no new Function, in any mode (runtime, the optional build-time compiler, or SSR). It goes further and auto-creates a Trusted Types policy named lit-html, routing its template HTML through it, so it works under require-trusted-types-for 'script' once you add lit-html to your trusted-types allowlist. Two honest carve-outs keep it from being a no-think adopt: the unsafeHTML / unsafeSVG directives apply no sanitization (audit them like any other raw sink), and inline style"…"= attributes (styleMap) can't be nonced — they fall under style-src-attr, so keep styling in static styles if you forbid inline styles. Net: the cleanest row in the table, and a useful proof that "needs eval" is a design choice, not an inherent cost of templating.

DOMPurify also emits TrustedHTML — Lit does too — but the kind it emits is the point. Lit's lit-html policy is a passthrough (createHTML: (s) => s): it vouches that a static, developer-authored string is what you wrote, which is safe only because the input was never untrusted. DOMPurify's policy sanitizes — it parses arbitrary, possibly-hostile HTML, strips the dangerous parts, and returns TrustedHTML that is genuinely safe to inject. That's the difference between "I promise this string is a constant" and "I promise this string can't hurt you," and it's why DOMPurify is the tool the "wrap" remedy reaches for: it's the one library here that makes untrusted input safe. You put it inside your Trusted Types policy, not beside it. Chart.js is a clean adopt today but wasn't always: its v1 template engine used new Function, removed in v2.0, and its v2 CSS injection was removed in v3 — a library improving toward CSP-safety, which is the common trajectory. And AngularJS 1.x is the cautionary tale: it can be coerced to run without unsafe-eval via ng-csp, but it is end-of-life and a documented script-gadget that bypasses allowlists, nonces, and even strict-dynamic. Loading it from a trusted host doesn't just risk your policy — it hands attackers a key to it. That's the difference between "needs a remedy" and "reject": some dependencies aren't incompatible with your CSP, they're corrosive to it.

"This library needs unsafe-eval" is, nine times in ten, "one feature of this library needs unsafe-eval, in a build I don't have to use." The gates aren't there to reject libraries — they're there to find the one feature that would have quietly forced the whole policy open.

Zooming out: the same test for a whole app

The per-library tree is what you run at npm install time. But if you're migrating an existing app to a strict CSP, you need the same logic applied across the codebase at once — not one dependency, but every place the app or its dependencies do something the policy forbids. The shape is the same; the inventory is bigger.

Migrating a whole app is the same four gates run across the codebase. Each row is a category of violation, what finds it, and the fix — the per-library remedies, applied in bulk.

The order that works in practice:

  1. Turn on the policy in report-only mode first. Content-Security-Policy-Report-Only sends violation reports without blocking anything, so you get a complete inventory of what would break before you break it. This is the single most important step; it converts "audit every dependency by hand" into "read the list the browser made for you."

  2. Triage the reports by the same four gates. Each violation is an eval, an inline script, a blocked sink, or a host-allowlist miss — the same categories, now with line numbers. Group them; the long tail is usually a handful of root causes.

  3. Refactor your own code; remedy the dependencies. Inline event handlers (onclick"…") become =addEventListener. javascript: URLs go. Your own innerHTML calls route through the Trusted Types policy. Then each offending dependency gets its tree-determined remedy: the no-eval build, the iframe, the Trusted Types hook, or the self-host.

  4. Enforce, and keep report-only running. Flip to the blocking header, but leave a report-only endpoint collecting — so the next dependency someone adds that violates the policy shows up as a report, not a production incident.

Report-only mode is the whole migration strategy in one header. It turns an intractable manual audit into a worklist the browser writes for you, and it keeps writing it forever — so the policy can't silently rot as the app grows.

What this costs, and why it's worth it

None of this is free. Every gate is a small tax on velocity: a library you'd have installed in seconds now needs a glance, sometimes a wrapper, sometimes a no. That tax is exactly the organizational friction the CSP piece is about — the policy making you say no, or "not like that," in a hundred small moments.

But the dependency graph is where most of the real attack surface lives, and a constraint that makes you look at what each dependency does at runtime is catching the same class of problem that supply-chain attacks exploit. The tree isn't only a CSP-compliance device; it's a habit of asking, of every package, "what does this actually do in my page?" — which is a question worth asking whether or not you ship the header.

The strict CSP is the forcing function, not the point. The point is that you now know, for every dependency you ship, whether it generates code, injects scripts, or writes to the DOM unsafely — and most teams have never had a reason to know.

Lessons

  • Under a strict CSP, adding a library stops being an npm-install decision and becomes a security review of what the code does at runtime.
  • eval, injected inline scripts, and raw innerHTML writes don't fail at build time — they throw at runtime, under the policy, in production, in the code path you didn't test.
  • Use the four gates — code generation, script injection, DOM sinks, third-party hosting — to score any front-end dependency systematically.
  • Apply the matching remedies: reject, sandbox (a separate-origin iframe), wrap (Trusted Types), or self-host; a dependency that needs none of them is a clean adopt.
  • Run migrations by using report-only mode to let the browser collect the exact worklist of violations for you.

References

  1. Trust No Script.” — what a strict CSP is, why allowlists fail, and the organizational cost of getting it right
  2. The Header That Can't Be Cached.” — the Cache-Control side of the same policy, and why nonced pages need no-store
  3. MDN: Trusted Types API.” MDN. — the mechanism behind the "wrap" remedy
  4. web.dev: Mitigate XSS with a strict CSP.” web.dev. — the deployment guide, including report-only rollout

How to cite

APA
Mangalapilly, Y. J. (2026, July). Can I Use This Library?. Saṃhitā Notes. https://yesudeep.com/blog/can-i-use-this-library/
BibTeX
@online{mangalapilly2026can,
          author  = {Yesudeep Jose Mangalapilly},
          title   = {Can I Use This Library?},
          journal = {Sa\d{m}hit\=a Notes},
          year    = {2026},
          month   = {July},
          url     = {https://yesudeep.com/blog/can-i-use-this-library/},
          urldate = {2026-07-03},
        }
Plain
Yesudeep Jose Mangalapilly. “Can I Use This Library?.” Saṃhitā Notes, 2026. https://yesudeep.com/blog/can-i-use-this-library/.
RIS
TY  - ELEC
        AU  - Mangalapilly, Yesudeep Jose
        TI  - Can I Use This Library?
        T2  - Saṃhitā Notes
        PY  - 2026
        UR  - https://yesudeep.com/blog/can-i-use-this-library/
        Y2  - 2026-07-03
        ER  - 

Annotations

Thank you — your note is held for review and will appear once approved.

Thank you — your note is published.

Please sign in below to leave a note.

Type to search · ↑↓ to move · ↵ to open · Esc to close