Content Security Policy (CSP) adoption has passed another milestone. According to the 2025 HTTP Archive Web Almanac, 21.9% of websites are now sending a CSP header, up from 18.5% the previous year. This seems like a promising direction, but among policies that used script-src to limit script sources, 92% included the 'unsafe-inline' keyword and 77% included 'unsafe-eval' – and those two keywords can remove much of the XSS protection developers expect CSP to provide.
This is the central problem with CSP misconfigurations: setting a CSP header isn’t the same as having an effective CSP policy. A policy can be present, syntactically valid, and visible in every security checklist while still permitting script behaviors that can allow successful cross-site scripting attacks. This article explains how those weak policies arise, which directives developers commonly misconfigure or omit, and how to build a CSP that provides useful protection without repeatedly breaking the application.

A CSP misconfiguration is a policy that is missing, overly permissive, incorrectly delivered, or inconsistent with the application’s actual resource-loading behavior.
CSP lets an application tell the browser which scripts, styles, images, frames, and other resources it may load or execute. A restrictive policy can block many script sources outright and limit the damage from an exploited cross-site scripting (XSS) vulnerability by preventing injected JavaScript from running or loading additional code.
CSP provides defense in depth. It doesn’t fix unsafe output encoding, dangerous DOM operations, template injection, or other underlying flaws. The application still needs to prevent XSS at source, but a well-designed CSP gives the browser another opportunity to stop malicious code when those primary controls fail.
Adding the CSP header is the easy part – writing a policy that matches a real application in its production context is much harder.
A strict policy may initially block inline scripts, tag managers, analytics code, chat widgets, dynamically loaded dependencies, and framework-generated content. Faced with a broken production candidate and a release deadline, developers understandably start by widening the policy just to unblock functionality.
Frequently, the first step is adding 'unsafe-inline' to script-src so you get all the inline scripts running. The application works again, the console errors disappear, and the security review still sees a CSP header. Unfortunately, if that’s all you do, the browser can now execute the same broad category of inline JavaScript that CSP was meant to restrict.
The result is a policy that satisfies the immediate operational requirements but provides far less protection than its presence promises.
There are two main ways that policies can fail: by explicitly allowing unsafe behavior and by skipping dedicated directives for content sources that don’t inherit from default-src and therefore remain unrestricted.
The most obvious CSP misconfigurations are values that make the script-src excessively permissive.
In a script-src directive, a value of 'unsafe-inline' generally permits inline <script> blocks, inline event handlers such as onclick and onerror, and javascript: URLs. This is a critical gap for XSS protection, since many XSS attacks rely on inline code rather than an external script request. When 'unsafe-inline' is specified, an attacker with enough control over the generated markup may be able to execute JavaScript directly in the page.
There are a few important exceptions. In modern browsers, 'unsafe-inline' is ignored when the same script-src directive contains a valid nonce or hash. When combined with 'strict-dynamic', a nonce or hash in that same directive also changes how the browser treats host and scheme allowlists. However, the mere presence of 'unsafe-inline' is often a strong indication that the policy needs review.
The 'unsafe-eval' keyword permits JavaScript APIs that turn strings into executable code, including eval() and new Function(). It also affects string arguments passed to functions such as setTimeout() and setInterval(). The presence of 'unsafe-eval' doesn’t directly create a vulnerability, but it does remove a CSP restriction on dangerous execution paths. If attacker-controlled data reaches one of those APIs, the browser will not use CSP to block the resulting code evaluation.
Some frameworks, development builds, and legacy libraries have historically depended on eval-like behavior. That can make 'unsafe-eval' difficult to remove, but the right response is to identify the dependency and determine whether it can be upgraded, reconfigured, or isolated – not to treat the keyword as harmless.
Policies such as script-src https: allow scripts from any HTTPS origin, and a bare wildcard is broader still. This is not directly equivalent to 'unsafe-inline' – because an attacker still needs a way to load a suitable external resource – but it effectively disables any meaningful origin restriction. Any attacker-controlled HTTPS host becomes an eligible script source, assuming the attacker can inject or influence a script URL.
Broad host allowlists create a related problem. Trusting an entire content delivery network, cloud storage domain, or third-party platform may also trust user-controlled locations, legacy JSONP endpoints, or redirect behavior hosted on that domain. An origin is not automatically trustworthy simply because the organization recognizes its name.
Allowing data: as a script source permits JavaScript to be embedded directly in a data URL. The CSP Level 3 specification specifically advises developers not to include data: or 'unsafe-inline' as valid script sources because both can enable code to be included directly in a document.
The following incorrect CSP implementation seems restrictive at first glance but allows several dangerous script behaviors:
# Unsafe policy example – do not use in production
Content-Security-Policy: default-src 'self' https: data: 'unsafe-inline' 'unsafe-eval'; script-src https: data: 'unsafe-inline' 'unsafe-eval';The opening self creates an impression of tight same-origin restriction, but the explicit script-src overrides default-src for scripts. Under this policy, the browser can load scripts from any HTTPS origin or a data URL, execute inline JavaScript, and evaluate strings as code.
Note that even this policy may still restrict some non-script resource types, so it isn’t literally identical to having no CSP, but it provides very little XSS mitigation.
A common misconception is that default-src 'self' sets a secure default fallback for every type of browser behavior in the CSP. Unfortunately, it doesn’t.
default-src provides a fallback for many fetch directives, including script-src, style-src, and img-src. The following important directives have no default-src fallback and must be declared explicitly:
Modern browsers no longer support the old plugin ecosystem that made embedded objects especially dangerous, but there is rarely a good reason for a modern application to allow arbitrary <object> or <embed> content.
Setting object-src 'none' removes an unnecessary content-loading path and is part of a sensible restrictive baseline.
The <base> element changes how the browser resolves relative URLs. If an attacker can inject a malicious <base> element, relative script paths, links, and other URLs may resolve against an attacker-controlled origin.
This issue is particularly relevant to nonce-based policies. A legitimate script tag with a valid nonce may still load from the wrong location if its relative src is retargeted through an injected base URL. The CSP specification therefore recommends restricting base-uri, typically by setting it to 'self' or 'none'.
The form-action directive controls where forms may submit data, and it doesn’t inherit from default-src. Without it, an injected or modified form may submit credentials or other user data to an unexpected destination even when the application has tightly restricted scripts. Set the directive according to actual form behavior rather than assuming other source restrictions cover it.
frame-ancestors controls which sites may embed a page and is the CSP mechanism used to reduce the risk of clickjacking.
This directive must be delivered through an HTTP response header – browsers ignore frame-ancestors inside a CSP <meta> element. Even so, the 2025 Web Almanac found that more than 2% of pages using meta-delivered CSP still attempted to set frame-ancestors there. It also found thousands of sites trying to place the separate X-Frame-Options header in a <meta> tag, where it is also ignored by browsers.
CSP has to describe the application the browser actually runs, not the application shown in an architecture diagram.
A production page may load first-party bundles, code-split chunks, fonts, images, analytics tags, payment integrations, consent managers, customer support tools, and scripts loaded by other scripts. Some dependencies may change behavior even without an application release, while others will differ across environments, regions, or user states. Allowlisting every edge case by hostname is tedious and brittle, while making the list broader keeps the application working but may weaken the policy.
AI-assisted development adds another route to the same outcome. A coding tool asked to "add a CSP header" may produce a plausible generic policy that contains 'unsafe-inline', 'unsafe-eval', and broad schemes among a long list of familiar third-party domains. Even if it starts with a stricter version, a follow-up request to fix application errors caused by the restrictions can encourage the tool to widen the policy further.
That doesn’t mean developers should stop using AI coding tools, but it does mean that generated security configurations need the same level of review as generated authentication logic, database queries, or access-control code. A CSP can look polished while encoding assumptions that are inappropriate for the application.
Third-party complexity creates a more fundamental challenge. If a trusted script can load further code, a static hostname allowlist may never fully describe its behavior. That is one reason modern CSP guidance favors trust based on nonces and hashes rather than increasingly broad lists of origins.
The most practical route to a stronger CSP is to establish explicit trust for individual scripts, remove unsafe fallbacks, and test the policy against real application behavior before enforcing it.
A CSP nonce is a cryptographically random value generated for one HTTP response. The server places it in both the Content-Security-Policy header and the nonce attribute of each legitimate script element. The browser executes a script only when its nonce matches the value in the policy.
An example of nonce usage in the CSP header and on the page might be:
# Illustration only – generate a new random value for every response
Content-Security-Policy: script-src 'nonce-rAnd0m-N0nce-Here'; base-uri 'self'; object-src 'none';<script nonce="rAnd0m-N0nce-Here">
initializeApplication();
</script>A production nonce must be generated server-side for every response using a cryptographically secure random number generator. The CSP specification recommends a length of at least 128 bits before encoding. Never hardcode the value, reuse it across responses, derive it from predictable data, or insert it into user-controlled markup – otherwise you’re weakening the security it provides.
Nonce deployment is an application change, not merely a header change. Templates, rendering middleware, caching, and edge delivery may all need adjustment so that the same per-request value reaches the header and every approved script element.
A CSP hash is useful when an inline script is static. Calculate a SHA-256, SHA-384, or SHA-512 digest of the script’s exact contents, Base64-encode it, and include the result in script-src, for example:
# Illustration only – replace the placeholder with the exact script hash
Content-Security-Policy: script-src 'sha256-BASE64_ENCODED_HASH'; base-uri 'self'; object-src 'none';The browser calculates the hash value before execution and runs the script only when it matches the policy. A matching hash value means the script is exactly the expected one – even a whitespace change will produce a different digest.
Hashes work well for small, stable snippets and build-generated assets. They are less convenient for scripts whose contents vary by request or deployment.
The 'strict-dynamic' keyword lets trust propagate from a nonce- or hash-authorized script to scripts that it loads dynamically.
This is useful for loaders and modern applications that create script elements at runtime. Instead of maintaining a host allowlist for every transitive dependency, the policy trusts an initial script and allows that script to load further code:
# Illustration only – use a fresh nonce for each response
Content-Security-Policy: script-src 'nonce-rAnd0m-N0nce-Here' 'strict-dynamic'; base-uri 'self'; object-src 'none';In CSP Level 3 browsers, 'strict-dynamic' causes host sources, scheme sources, 'self', and 'unsafe-inline' to be ignored for script loading when nonce or hash trust is present.
That simplifies deployment, but it changes the trust model. A nonce-bearing loader can now authorize additional scripts, so developers need to review how trusted code constructs script URLs.
If attacker-controlled input can determine what a trusted loader imports, 'strict-dynamic' may allow that import.Legacy note: The CSP specification documents a backward-compatible pattern that combines 'unsafe-inline', https:, a nonce, and 'strict-dynamic'. Modern browsers reduce this to nonce-based dynamic trust, while older implementations fall back to weaker but specified behavior. This pattern should not be used by default and is only relevant when an application absolutely must support pre-CSP2 browsers, which represent negligible production traffic for most applications in 2026.
A practical starting point for the directives not covered by default-src is:
# Illustration only – adapt framing and form destinations to the application
Content-Security-Policy: default-src 'self'; script-src 'nonce-rAnd0m-N0nce-Here' 'strict-dynamic'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self';From this starting point, applications may need separate rules for styles, images, fonts, frames, workers, connections, and media. Some applications should use base-uri 'none' or frame-ancestors 'none', while others legitimately need specific external form or framing destinations.
Whenever you set a narrow CSP, the policy should be narrow because the application is understood, not because you’re trying to guess production behavior.
Content-Security-Policy-Report-Only lets browsers evaluate a policy and report violations without blocking resources. It is the safest way to observe what a proposed policy would affect before turning it into an enforcing control:
# Illustration only – configure reporting for your own endpoint
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'nonce-rAnd0m-N0nce-Here' 'strict-dynamic'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; report-to csp-endpoint;Report-Only mode must be delivered as an HTTP response header and cannot be enabled through a <meta> element. The same restriction applies to report-uri, frame-ancestors, and sandbox inside meta-delivered policies.
A useful rollout sequence is:
Content-Security-Policy-Report-Only.Content-Security-Policy header.You can run a Report-Only policy alongside the enforcing CSP header to evaluate proposed directive changes before enforcing them.
Violation reports are telemetry, not a perfect source of truth. Browser extensions, injected software, bots, and malformed reports can create noise. Treat reports as evidence to investigate rather than reasons to widen the policy automatically to make the errors go away.
A manual review can produce an excellent policy for one application response at one point in time. It is less effective at identifying missing headers on forgotten routes, inconsistent policies between environments, unsupported meta-tag usage, or regressions introduced by infrastructure changes.
Because CSP security checks are passive, a dynamic application security testing (DAST) scanner evaluates the headers and directives it observes without attempting to exploit the configuration. It can identify conditions such as a missing CSP header, unsafe source expressions, invalid syntax, misplaced directives, and inconsistent application of the policy.
The Invicti platform includes a DAST scanner that checks both for CSP header presence and for configuration problems that can make a policy ineffective. Its CSP coverage includes more than 20 checks for directive syntax and unsafe values, allowing teams to track these issues alongside other findings from the running application.
Regular scanning doesn’t replace design review or Report-Only testing – but it does provide repeatable coverage across reachable application pages and helps stop a policy that was carefully built at launch from quietly degrading into script-src * 'unsafe-inline' six releases later.
In script-src, a value of 'unsafe-inline' generally allows inline script blocks, event-handler attributes, and javascript: URLs to execute, in effect bypassing a lot of CSP’s cross-site scripting protection. Modern browsers ignore the value when a valid nonce or hash appears in the same directive – placing a nonce elsewhere in the header does not have that effect.
A CSP nonce is a random value generated for each response and attached to approved script elements. A CSP hash represents the exact contents of a stable script and remains valid while those contents don’t change. Nonces suit dynamic server-rendered pages, while hashes suit fixed inline scripts and build artifacts.
The 'strict-dynamic' value for script-src works with a nonce or hash to let an approved script load additional scripts dynamically. This avoids maintaining a brittle hostname allowlist for every script loaded by a trusted loader or framework. It does not make dynamically loaded code inherently safe – the trusted script must still prevent attacker-controlled input from determining what it imports.
default-src 'self' allows same-origin scripts, so it may still trust vulnerable or attacker-influenced script endpoints hosted by the application. It also doesn’t fix any underlying injection flaw. A nonce- or hash-based script-src, combined with secure coding controls, provides stronger protection than a same-origin allowlist alone.
An enforcing CSP can be delivered through a <meta http-equiv="Content-Security-Policy"> element, but the policy only applies to content that appears after the element and only on a specific page. Report-Only mode isn’t supported in meta tags, and browsers ignore frame-ancestors, sandbox, and report-uri there. HTTP response headers are the preferred delivery mechanism for CSP.
CSP Report-Only mode evaluates a policy and reports violations without blocking content. It lets teams observe how a stricter policy would affect the application before enforcement. It must be configured using the Content-Security-Policy-Report-Only HTTP response header.
Frequently omitted directives include object-src, base-uri, form-action, and frame-ancestors. These directives don’t inherit restrictions set using default-src, so developers must set them explicitly to restrict embedded objects, base URLs, form destinations, and framing origins.
