A Content Security Policy (CSP) can reduce the damage caused by injected scripts, but an overly strict policy can also break legitimate functionality. This guide explains how to design a CSP, test it safely with report-only mode, diagnose blocked resources, and update the policy as your frontend and third-party services change.
Overview
CSP is an HTTP response header that tells a browser which sources are allowed for scripts, stylesheets, images, fonts, frames, connections, and other resource types. A policy is made of directives such as script-src, style-src, img-src, and connect-src. Each directive contains one or more source expressions, such as 'self', a trusted origin, a nonce, or a hash.
The policy should describe what your site actually needs, not every domain that might be useful in the future. Start with an inventory of resources loaded by a representative set of pages, then tighten the policy over time. A browser-based developer tool, a staging deployment, and the browser console are usually enough to begin; a Content-Security-Policy generator can help assemble observations, but its output still requires review.
There are two practical delivery modes:
- Report-only: the browser reports violations without blocking the resource. Use this while discovering dependencies and fixing false positives.
- Enforced: the browser blocks resources that do not match the policy. Move to enforcement only after testing important user journeys.
A minimal starting point might look like this:
Content-Security-Policy: default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
This is not a universal production policy. It establishes useful restrictions, but your application may also need carefully selected rules for scripts, styles, images, fonts, API calls, frames, or form destinations.
Checklist by scenario
For a mostly static site
- List the origins used for scripts, styles, fonts, images, and analytics.
- Set
default-src 'self'as a fallback, then add only the directives that need different behavior. - Use
object-src 'none'if legacy plugin content is not required. - Set
base-uri 'self'to restrict the document base URL. - Use
frame-ancestorsto control which sites may embed the page.
Example:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' https://fonts.example.com; font-src 'self' https://fonts.example.com; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
For a single-page application
- Identify the JavaScript bundle origin and every API or WebSocket origin used by the application.
- Put API and WebSocket destinations in
connect-src; do not assume thatscript-srcpermits network requests. - Check whether the build system injects inline scripts, runtime configuration, or inline styles.
- Prefer nonces or hashes for required inline code instead of broadly allowing
'unsafe-inline'. - Test client-side navigation, authentication, error pages, uploads, and embedded widgets.
Example structure:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{per-response-value}'; connect-src 'self' https://api.example.com wss://socket.example.com; style-src 'self'; img-src 'self' data: https://images.example.com; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
A nonce must be unpredictable, generated for the response, and placed on the matching script element. Do not reuse one fixed nonce across requests. If a script is static and does not change, a CSP hash can be appropriate, but the hash must match the exact script content.
For a site using analytics, payments, or embedded services
- Document each vendor by resource type: script, frame, image, font, connection, or form destination.
- Allow the smallest required origin set for each directive.
- Test the complete checkout, login, consent, and account workflows rather than only the homepage.
- Review vendor documentation for redirects, iframe origins, API endpoints, and dynamically loaded assets.
- Remove a vendor from the policy when the integration is removed from the application.
For example, an embedded payment flow may require frame-src, while a client-side payment library may also need script-src and connect-src. These permissions are separate; adding one does not automatically cover the others.
For testing a new policy
- Deploy the candidate policy as
Content-Security-Policy-Report-Only. - Exercise important pages and user flows in more than one browser.
- Collect violation reports or inspect browser console messages.
- For each violation, decide whether the resource is required, replaceable, or unexpected.
- Move the reviewed policy to
Content-Security-Policyand continue monitoring after deployment.
Report-only mode is a test aid, not a security boundary. It tells you what would be blocked but does not prevent the request or execution.
What to double-check
| Directive | Review it when you need to control |
|---|---|
default-src | Fallback behavior for resource types without a more specific directive. |
script-src | JavaScript files, inline scripts, nonces, hashes, and script hosts. |
style-src | Stylesheets, inline styles, and style-related nonces or hashes. |
img-src | Images, data URLs, blobs, avatars, tracking pixels, and image CDNs. |
font-src | Webfont origins and font loading from a CDN. |
connect-src | Fetch, XHR, EventSource, WebSocket, and other browser connections. |
frame-src | Origins that the page is allowed to embed in frames. |
frame-ancestors | Sites allowed to embed your page. |
form-action | Destinations that can receive submitted forms. |
object-src | Legacy plugin-style content; 'none' is often suitable when unused. |
Check the response headers in the browser’s Network panel, not only the server configuration. A reverse proxy, CDN, framework adapter, or hosting platform may add, replace, or combine headers. Also check whether multiple CSP headers are being sent; their interaction can be more restrictive than expected.
When reviewing a violation, record the blocked URL, directive named in the message, page route, user action, and whether the request was expected. Pay attention to the effective origin, including scheme, host, port, and redirects. A rule for one hostname may not cover a redirected destination or a separate API subdomain.
Common mistakes
- Using a broad wildcard:
*can make the policy appear to work while granting access to more origins than intended. Prefer explicit origins and resource-specific directives. - Adding
'unsafe-inline'immediately: This often hides the real source of inline code. First determine whether a nonce, hash, or small code refactor can solve the requirement. - Confusing report-only with protection: A report-only header does not block an attack or a broken resource. Treat it as an observation phase.
- Allowing a whole vendor for one feature: A third-party script may need one host, while images or connections use another. Separate permissions by directive.
- Testing only the homepage: Login, checkout, administration, forms, media, and error routes often load different resources.
- Ignoring inline event handlers: HTML such as
onclickcan require a different approach from a nonce-bearing script element. Prefer unobtrusive event listeners where practical. - Forgetting operational dependencies: Build previews, monitoring dashboards, consent tools, and support widgets can introduce resources that are absent locally.
- Relying on CSP alone: CSP is one layer of browser security. Continue to address output encoding, input handling, dependency updates, authentication, access control, and other protections.
If a blocked script appears unexpectedly, do not simply copy the reported domain into script-src. Confirm ownership, purpose, transport security, data handling, and whether the application still needs it. This investigation is especially important when a policy violation involves an unfamiliar host.
When to revisit
Review the CSP whenever the inputs change. Create a small policy checklist for each release or infrastructure change:
- After adding, removing, or replacing an analytics, payment, chat, consent, or advertising integration.
- When a frontend framework, bundler, hosting provider, CDN, or reverse proxy changes.
- When a new API subdomain, image store, font provider, WebSocket endpoint, or embedded service is introduced.
- After changing inline configuration, server-rendered templates, nonce generation, or build output.
- When production users report missing styles, blank components, failed logins, broken media, or checkout errors.
- Before a major seasonal campaign or traffic period, when important flows deserve a fresh end-to-end test.
Keep a short change record containing the policy version, reason for each allowed origin, test routes, and unresolved report-only findings. Revisit that record when workflows or tools change. A useful final check is to validate the response on a real deployed environment, inspect the console, and complete the site’s critical journeys. If a policy becomes difficult to understand, split the investigation into directives and resource types rather than adding broad permissions. A clear, tested policy is easier to maintain and safer to tighten than a long list assembled from unexplained browser errors.