All resources
Web Security11 min read

Content Security Policy as an engineering control

Build a CSP that reduces script injection risk, supports modern deployments, and can be rolled out without breaking legitimate features.

A practical PingFlow guide for developers working at the boundary between systems.

At a glance

Key takeaways

  • CSP limits what a page may execute
  • Use report-only before enforcement
  • Prefer nonces or hashes over unsafe inline code
In this guide

CSP limits what a page may execute

Content Security Policy lets a site tell the browser which sources may provide scripts, styles, images, frames, fonts, connections, and other resources. It is a defense-in-depth control against injection and unsafe dependencies. CSP does not sanitize HTML, validate server input, or make a vulnerable framework safe by itself. Its value comes from reducing the number of ways an unexpected string can become executable code.

Begin with an inventory of how the page actually loads. List bundled scripts, third-party analytics, payment frames, fonts, images, API connections, workers, and inline styles or scripts. A policy written from a template often blocks a legitimate product feature or adds a broad wildcard that provides little protection. Treat the current application behavior as evidence, not as an assumption about what the framework emits.

Use report-only before enforcement

Content-Security-Policy-Report-Only lets you observe violations without blocking resources. Deploy it with a reporting endpoint or browser reporting mechanism, collect violations, group them by directive and source, and fix or classify each one. A report may come from an extension, a user-provided page, a browser quirk, or a real application dependency. Protect the report endpoint from unbounded payloads and redact sensitive URLs before retaining them.

Do not treat report volume as proof that every violation is exploitable. A report tells you that the browser observed a policy mismatch. It does not include all the context needed to determine impact. Still, every unexpected source deserves an owner and a decision. Once the report-only stream is quiet across a representative release cycle, move to enforcement with a rollback plan.

Prefer nonces or hashes over unsafe inline code

A strict script policy avoids unsafe-inline and allows only scripts that carry a per-response nonce or match a known hash. A nonce must be unpredictable, generated for each response, placed on the intended script tag, and included in the policy. Never reuse a nonce across requests or expose it in a client-controlled value. Hashes can work for static inline scripts, but any content change requires a new hash.

Frameworks and third-party widgets may require a nonce to be threaded through server rendering, streaming, or a payment frame. Do not add unsafe-eval or a wildcard script source casually because a development tool failed. Separate development and production policies when necessary, and make the weaker mode impossible to ship accidentally. The goal is a narrow allowance for known behavior, not a list that grows until every script is trusted.

Control connections and embedded content

script-src is only one part of CSP. connect-src controls fetch, WebSocket, and other programmatic connections. frame-src controls which frames the page may embed, while frame-ancestors controls which sites may embed your page. img-src, font-src, style-src, worker-src, object-src, and base-uri close other common escape paths. Set object-src to none and base-uri to self or none when the application does not need legacy plugin content or dynamic base URLs.

List only the origins the product requires, including the production API, analytics endpoint, payment provider, and asset host. A wildcard subdomain can include an abandoned host or a user-controlled tenant. Review redirects and DNS ownership for every allowed source. If an external provider changes domains, make the update an intentional security change rather than accepting all destinations to get the page working again.

Coordinate CSP with other headers

CSP works with other browser controls. Strict-Transport-Security forces HTTPS after a safe rollout. X-Content-Type-Options prevents MIME sniffing. Referrer-Policy reduces URL leakage, and Permissions-Policy limits browser capabilities. Frame-ancestors and the older X-Frame-Options should agree. A header set is a system; changing one directive can make another redundant or create a confusing exception.

Keep policy generation close to the response boundary so every route receives the intended headers. Verify static assets, error pages, API responses, and redirects. A policy on the homepage does not protect a separate embedded tool route if that route emits its own headers. Test through the CDN and Cloud Run edge, not only through a local development server.

Make reports useful without leaking data

Violation reports can contain blocked URLs, document locations, and snippets that reveal user input. Store only the fields needed to group and remediate a violation, apply a retention limit, and restrict access. Rate-limit the endpoint because a page can generate many reports under a broken policy or an attack. Avoid using the report body as an unvalidated analytics event.

Track policy version, route, browser family, directive, blocked source, and whether the resource was actually blocked. Alerts should focus on new production sources or a spike after deployment. A report that repeats for a known browser extension should not page the application owner, but a new script host on a checkout route should.

Test policy changes as releases

Use automated browser tests for login, checkout, analytics, WebSocket or webhook dashboards, frames, workers, and error pages. Test nonce presence and uniqueness, blocked inline scripts, disallowed connections, and a legitimate third-party source. Run report-only in staging and production before enforcement. Include a canary release so a policy mistake does not block every user at once.

CSP is strongest when it is maintained like code. Inventory sources, observe violations, remove broad allowances, coordinate related headers, and assign an owner for every exception. A policy should make an injection harder without making the team afraid to change the frontend. That balance comes from evidence and incremental enforcement, not from copying a long header from another site.

Treat exceptions as temporary code

Every source added to a CSP should have a reason, owner, route scope, and review date. Prefer a nonce, hash, or narrow host over a broad wildcard. When a third-party script is removed, delete its policy allowance and report configuration. Stale allowances are difficult to notice because they do not affect the page until an injection or dependency compromise occurs.

Include CSP checks in a normal frontend release. A new analytics provider, payment frame, worker, or font host should fail a review if it has no policy decision. Keep report-only available for a controlled migration, but do not leave a permanent report-only policy that creates the appearance of protection without blocking anything.

Implementation example

Start with a report-only policy that inventories scripts, styles, frames, images, fonts, connections, workers, and form targets. Replace broad hosts with nonces, hashes, or narrow origins where possible, then enforce the policy after violations are classified. Keep third-party script ownership and removal dates beside the exception.

http
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-...'; connect-src 'self' https://api.example.com; frame-ancestors 'none'

Verify and troubleshoot

Exercise authenticated and unauthenticated routes, inline script paths, workers, payment frames, analytics, image uploads, and error pages. Collect report-only violations, group them by directive and source, and verify that a blocked resource is not silently replaced by an unsafe fallback. Test the enforced policy in a canary browser session before broad rollout.

Operations and recovery

Review every CSP exception when dependencies, domains, or frameworks change. Protect the report endpoint from unbounded payloads and redact sensitive URLs. If a policy blocks a critical path, revert the narrow directive or route policy with an owner and expiry; do not switch the entire application back to a wildcard policy.

References and further reading

Use CSP Level 3, MDN directive guidance, and the OWASP Content Security Policy Cheat Sheet. Treat CSP as defense in depth alongside output encoding, input validation, and trusted dependency management.

Keep exploring