A web application can escape database queries correctly, authenticate every API request, and still hand an attacker code execution in the browser through one unsafe rendering path. The browser is unusually permissive by design: HTML can load scripts from remote origins, inline blocks can execute code, and dynamic DOM operations can turn strings into active content. Content Security Policy, or CSP, gives an application a second control plane for that execution environment.

The useful security property is not a long list of trusted domains. It is the ability to make executable content prove that the application authorized it. Modern nonce- and hash-based policies can separate markup that merely appears in a document from script that is permitted to run. That distinction changes the consequences of many injection defects. An attacker may still place HTML into a page, yet arbitrary JavaScript does not automatically follow.

CSP remains a mitigation rather than a substitute for safe rendering. Its value comes from containing failures that survive other controls, while also forcing teams to identify the script relationships their pages actually depend on.

Script execution is the boundary that matters

A policy such as script-src 'self' looks restrictive because it limits script loads to the application’s own origin. In practice, same-origin trust can be broad. Applications often host user-controlled files, legacy endpoints, JSON-like resources, redirectors, or script gadgets under that origin. Treating every path on a host as equally suitable for executable content can preserve more authority than the policy appears to grant.

Host allowlists have a similar structural problem. A third-party domain may serve many independent applications or permit content publication by multiple tenants. If any allowed origin can return attacker-influenced JavaScript, the browser sees a permitted source rather than the organizational boundary engineers had in mind.

Nonce-based CSP approaches the problem from a different direction. The server generates an unpredictable nonce for each response, places it in the CSP header, and attaches the matching value only to script elements authorized for that document. Injected markup does not know the nonce in advance, so a newly inserted inline script lacks the browser’s authorization to execute.

A hash-based policy can provide the same form of explicit approval for static inline script content. The policy carries a cryptographic hash of the exact script bytes. Any modification changes the digest and loses authorization. Hashes fit stable content; nonces fit server-rendered pages where approved scripts can receive a fresh token per response.

Neither mechanism makes injected HTML harmless. Markup can still alter presentation, submit forms, load some resource classes allowed by policy, or exploit application-specific behavior. CSP narrows one especially powerful transition: turning injected content into unrestricted script execution.

Nonces are capabilities, not decorative attributes

A nonce only has security value when it is unpredictable and scoped to the response. Reusing a fixed nonce across requests turns a secret authorization value into a constant an attacker can copy. Generating it in client-side code is also too late; the browser needs the policy and authorization relationship as it parses and evaluates executable content.

Template systems can create a subtler failure. Some implementations mechanically add the nonce attribute to every script element after parsing or rendering. If attacker-controlled markup can enter that processing path, the application may bless the attacker’s script itself. The control works only when nonce placement follows trusted application structure.

Nonce exposure inside the page is not, by itself, equivalent to a password disclosure. Browser CSP processing includes protections around nonce attributes, and legitimate scripts necessarily operate in a document associated with the nonce. Still, application code should not treat the value as general-purpose data or copy it onto script elements derived from untrusted input. The important property is authorization provenance: trusted rendering decides which scripts receive permission.

This also puts pressure on server-side caching. A cached HTML response containing a nonce must remain consistent with its CSP header, and broad reuse of the same response can extend nonce lifetime far beyond the intended request scope. Systems that cache rendered HTML need a design that preserves per-response authorization or use another policy model suited to their delivery architecture.

strict-dynamic shifts trust from locations to approved code

Modern CSP supports the strict-dynamic source expression for script policy. When paired with a valid nonce or hash, it allows trust to propagate from an authorized script to scripts that it loads dynamically through script elements. This is valuable for applications whose bootstrapping code loads bundles, chunks, or other runtime dependencies.

The model is materially different from maintaining an expanding host allowlist. The initial script must carry cryptographic authorization from the document policy. Code loaded by that trusted script can then participate in the execution graph without requiring every destination host to become an independent root of trust.

That propagation deserves careful interpretation. strict-dynamic does not inspect the semantic intent of a script. If a nonce-authorized bootstrapper can be manipulated into loading an attacker-selected URL, the application has placed dangerous authority inside trusted code. CSP can establish where trust starts, but application logic still determines how that trust is exercised.

This is one reason script gadgets matter. A page may block direct attacker scripts yet contain trusted JavaScript that reads attacker-controlled DOM data and passes it into dangerous execution or loading primitives. A strong policy removes many easy paths, but it cannot compensate for every capability exposed by code that already has permission to execute.

Compatibility can hide policy drift

CSP has accumulated features across browser generations, so production policies often contain fallback expressions for older clients. A policy may include a nonce, strict-dynamic, and host sources together. Modern browsers can apply the nonce-centered trust model while older implementations fall back to source lists they understand.

That compatibility pattern can make reviews deceptive. Engineers reading the header may assume all listed sources remain active in modern clients, or may remove a fallback without recognizing the browser population it serves. Policy maintenance needs to distinguish deliberate compatibility syntax from active trust roots.

Multiple CSP headers add another operational detail. Browsers enforce all applicable policies; an additional policy does not simply overwrite the earlier one. This can make accidental tightening visible as broken resources, but it also means debugging requires examining the complete response rather than a single configuration fragment.

Report-Only mode is useful during deployment because violations can be observed without blocking content. It should not be mistaken for enforcement. Reports are telemetry, can be noisy, and may be absent because of client behavior or delivery failures. They help reveal dependencies and candidate breakage; the security boundary appears only when an enforcing policy is delivered.

Browser policy cannot repair unsafe data flows

The strongest CSP still sits downstream of application decisions. Context-aware output encoding, safe DOM APIs, robust template behavior, and careful handling of untrusted URLs remain primary controls. If an application passes attacker-controlled text to an execution primitive inside an already authorized script, the policy may have no additional decision to make.

The same principle applies beyond JavaScript. CSP directives govern several resource and navigation classes, but each directive has its own semantics. A narrow script policy does not automatically restrict frames, connections, images, form targets, or base URL behavior. Security reviews should evaluate the directives that correspond to the application’s actual attack surface instead of treating the presence of a CSP header as a binary property.

object-src 'none' and a restrictive base-uri, for example, commonly complement a nonce-centered script policy because they close separate browser behaviors. frame-ancestors addresses embedding by other pages and is conceptually distinct from script authorization. The policy becomes effective when its directives express intentional boundaries, not when it accumulates a large number of tokens.

There is also a deployment consequence: CSP makes hidden browser dependencies observable. A page that silently relies on inline event handlers, string-to-code conversion, or scripts from loosely governed domains becomes harder to place under a strict policy. The resulting breakage is inconvenient, but it exposes architectural trust that already existed.

A policy is strongest when authorization stays narrow

CSP is often introduced as an HTTP header, but its deeper role is architectural. A nonce or hash can make script permission explicit at the point where server-rendered trust enters the browser. strict-dynamic can carry that permission through a controlled loading graph. Reporting can reveal places where old assumptions resist that model.

The durable security gain comes from keeping the roots of execution authority small. If every template fragment receives a nonce, every host joins an allowlist, or trusted scripts accept arbitrary execution inputs, the policy gradually recreates the permissive environment it was meant to constrain.

A well-maintained CSP therefore acts less like a filter pasted onto a finished application and more like a record of browser-side authority. It shows which code may begin execution, which code may extend that trust, and where application design still relies on ambient permission. That record is valuable even before an injection defect appears, because it makes the browser’s execution boundary something the application must state deliberately.