Content Security Policy Makes Script Authority Explicit

A browser does not distinguish between JavaScript that a development team intended to ship and JavaScript that arrived through an injection flaw. Once script markup becomes part of a document and passes the browser’s normal parsing rules, it can execute with the authority of that origin. Escaping and contextual output encoding remain primary defenses against injection, but a single missed boundary can still turn untrusted text into active code.

Content Security Policy, or CSP, adds a separate decision point. A site sends policy that limits the sources, execution modes, embedding relationships, and other resource behaviors the browser will accept. The result is not a replacement for safe application code. It is a browser-enforced constraint on what compromised markup can accomplish.

That distinction matters operationally. CSP is most effective when it expresses a narrow model of legitimate page behavior rather than serving as a long list of domains accumulated over time.

Script permission is more precise than host permission

Early CSP deployments often centered on host allowlists. A script-src directive could permit scripts from the site’s own origin and selected external hosts. This blocks code from arbitrary locations, but host trust is broader than it first appears. A permitted host may serve user-controlled files, JSONP endpoints, legacy applications, or other content that was never meant to become executable in the protected page.

Modern policies can bind permission to individual script elements instead. A server can generate a cryptographically unpredictable nonce for each response, place that nonce in the CSP header, and attach the same value only to script elements authorized for that response. A script without the matching nonce is rejected even when injected into the document.

The nonce is an authorization token, not a decoration. It needs enough entropy to resist guessing and must be fresh for each response. Reusing a stable nonce across responses turns it into a value an attacker may be able to recover and reuse. Templates also need discipline: copying the nonce onto markup influenced by untrusted data can authorize code that the policy was meant to block.

Hashes provide another form of explicit script authorization. The policy can contain a digest of an expected inline script, allowing the browser to execute content whose bytes match that digest. Hashes fit static inline code well, but routine content changes require corresponding policy updates.

strict-dynamic shifts trust toward authorized loaders

A nonce-based policy can still become difficult to maintain when an authorized bootstrap script loads additional code at runtime. CSP Level 3 introduced the strict-dynamic source expression for this model. In supporting browsers, a nonce- or hash-authorized script can confer trust to scripts it loads programmatically. Host and scheme source expressions in the same script-src directive are ignored for script loading under that trust model.

This changes the unit of trust. Instead of declaring every script host acceptable, the policy can authorize a small set of initial scripts and allow those trusted scripts to construct the required dependency graph.

The mechanism also raises the value of the authorized loader. If that script contains an injection sink, imports attacker-selected URLs, or otherwise turns untrusted input into executable code, CSP cannot make the loader safe. The policy establishes authority; application logic still determines how that authority is used.

Compatibility also deserves deliberate treatment. Older browsers that do not implement strict-dynamic may fall back to other source expressions in the policy. A production policy therefore has to account for the browser population the application actually supports rather than assuming identical enforcement everywhere.

Inline event handlers expose legacy assumptions

Applications with long histories often depend on inline event handlers, inline scripts, string-to-code APIs, or third-party snippets that assume unrestricted execution. A strict policy makes those assumptions visible quickly.

The broad source expression 'unsafe-inline' permits inline script behavior that CSP would otherwise restrict. Adding it can restore a legacy page, but it also removes much of the protection a script policy was intended to provide. Similarly, 'unsafe-eval' permits several forms of string evaluation that strict deployments commonly seek to exclude.

These keywords are not inherently evidence of negligence. Some applications or dependencies still require them. Their presence is better treated as an architectural constraint with a known security cost. A migration can then focus on moving event handlers into script files, replacing dynamic code generation, or isolating components that cannot meet the primary policy.

CSP also separates directives by resource type. script-src controls script loading and execution rules, while directives such as img-src, connect-src, font-src, style-src, and frame-src govern other fetch or rendering contexts. default-src supplies a fallback for several fetch directives that are absent, but it does not act as a universal substitute for every CSP directive.

A policy can limit impact beyond script execution

Script injection receives much of CSP’s attention, yet several directives address different browser security boundaries.

frame-ancestors controls which parent documents may embed a page in frames or related containers. This is useful against unwanted framing and is conceptually separate from resource-fetch directives. It does not fall back to default-src.

object-src can restrict plugin-style embedded resources. A restrictive value is common on sites that have no legitimate need for them. base-uri constrains the URLs that a document’s base element may establish, reducing the scope for injected base elements to redirect relative URL resolution. form-action limits destinations for form submissions, which can constrain some data-exfiltration paths even when an attacker gains markup influence.

These controls illustrate a broader property of CSP: the browser has several forms of authority besides running a script. Loading a network resource, embedding a document, resolving a relative URL, and submitting a form each cross a distinct boundary. A useful policy describes the boundaries the application actually needs.

Reporting turns enforcement into operational evidence

Deploying a restrictive policy directly into enforcement can break production pages in subtle ways. Content may come from regional asset hosts, payment providers, support widgets, analytics systems, or code paths that appear only for certain accounts. CSP supports report-only operation so teams can observe violations without blocking the associated behavior.

A Content-Security-Policy-Report-Only header asks the browser to evaluate policy and report violations while leaving the content available. This can reveal legitimate dependencies and suspicious behavior before enforcement. It is also useful after enforcement begins, when a stricter candidate policy can run in report-only mode beside the active policy.

Reports need interpretation rather than automatic trust. Browser extensions, injected local software, stale clients, and unusual network environments can generate violations that do not represent defects in the site. Report endpoints can also receive substantial volume. Collection systems need rate controls, retention rules, and enough context to separate recurring application problems from ambient noise.

Report-only mode is an observation mechanism, not a security boundary. A policy starts blocking disallowed behavior only when delivered through the enforcing Content-Security-Policy header.

Multiple policies only narrow the effective result

CSP can arrive through more than one response header, and HTML can also carry policy through a meta element with limitations. When multiple enforced policies apply, the browser enforces all of them. Adding a second policy does not loosen a restrictive first policy; content has to satisfy each applicable policy.

This property can surprise teams operating through several infrastructure layers. An application may emit one header while a reverse proxy or edge service adds another. The resulting behavior reflects their intersection, not a last-header-wins configuration model.

The placement of policy also affects capability. HTTP response headers support the full delivery model and are preferable for controls that must apply before document processing reaches relevant markup. Meta-delivered policy has restrictions, including lack of support for directives such as frame-ancestors and report-only operation.

Operational ownership is therefore part of CSP design. Teams need to know which layer emits policy, which component generates nonces, and which deployment path updates hashes or source expressions. Otherwise a security control can become a source of unexplained production drift.

CSP is strongest as a constrained execution architecture

A permissive policy can satisfy a compliance checkbox while doing little against injection. Long host lists, unrestricted inline execution, broad schemes, and exceptions added during incidents gradually convert a security boundary into documentation of everything the page has ever loaded.

A stronger design starts from executable authority. A small set of response-specific nonces or stable hashes identifies the scripts that may begin execution. Dynamic loading is delegated only where the application architecture requires it. Other directives then restrict network destinations, framing, forms, and resource classes according to actual product behavior.

This model also keeps CSP in its proper place. It can reduce exploitability and contain some classes of browser-side compromise, but it cannot repair unsafe DOM APIs, validate server input, correct authorization flaws, or make a compromised trusted script benign. The policy is a second boundary whose value depends on independence from the defect it is expected to contain.

That independence is the central security property. When application code accidentally admits hostile markup, the browser still has a separately delivered statement of what the page is authorized to execute and where it is authorized to reach. A narrowly maintained CSP turns that statement into an enforceable part of the web application’s architecture.