Back to blog
ImplementationAugust 20, 20269 min readUpdated August 30, 2026

Content Security Policy for Website Chatbots: Safely Allowing Widgets, APIs, Images, and Streaming

A practical CSP for website chatbots only allows the scripts, API connections, streams, and images that are actually required—without unnecessary wildcards.

An adult event technician inspecting a secured connection module for a chatbot widget on a summery outdoor stage.

In a browser, a website chatbot rarely consists of a single JavaScript file. A loader opens the widget, an API accepts messages, responses are returned via a stream, and profile pictures or media might live on another domain altogether. A Content Security Policy (CSP) brings visibility to these paths and restricts them: the browser only loads or connects to what the website explicitly allows.

This provides an important second layer of defense against cross-site scripting and unexpected third-party content. However, a CSP does not fix an insecure API, missing authentication, poor input validation, or prompt injection. It reduces the scope for injected code and bounds the radius of a flaw. That is why a minimal, tested policy is crucial rather than a long list of broadly trusted domains.

Why Chatbot Widgets Need Special CSP Rules

For a traditional content page, resources from the same origin are often enough. A chatbot, on the other hand, keeps communicating after it loads. connect-src controls fetch(), XMLHttpRequest, EventSource, WebSocket, and sendBeacon(), among others. This is precisely where messages, streaming responses, feedback events, and telemetry flow. If the correct origin is missing, the widget may render, but it won’t be able to reply.

Other components fall under their own directives. script-src governs the widget loader, img-src covers avatars and response images, style-src manages stylesheets, and font-src controls external fonts. An iframe-based widget also requires frame-src. default-src acts as a fallback for many unlisted resource types, but it is no replacement for a deliberate inventory.

The most critical prep work happens in the browser, not in a CSP generator: open a representative page, start a conversation, let a long response stream in, open sources, send feedback, and test error and handoff scenarios. In the network panel, check the origins that are actually contacted. Document the purpose, resource type, and responsible owner for every host.

Safely Allowing the Four Main Data Paths Separately

1. Widget Script and Initialization

Fetch the loader from a stable, versioned address whenever possible. A rule like script-src https: is too permissive because it allows scripts from any HTTPS domain. Instead, specify the exact CDN origin or self-host the loader. If your integration relies on inline code, use a nonce generated fresh per HTTP response or a suitable hash. 'unsafe-inline' should not become a quick, permanent fix.

A nonce belongs only on scripts generated directly by your server-side template. Middleware that blindly appends the same nonce to every existing script tag will grant trust to injected tags as well. For a static, versioned third-party script, Subresource Integrity (SRI) can offer additional protection, provided the hash is updated reliably when files change.

2. API, Server-Sent Events, and WebSocket

Standard POST requests and streaming responses via fetch() require the HTTPS API origin in connect-src. Server-Sent Events via EventSource are covered by this as well. For a WebSocket, explicitly add the concrete wss:// origin. MDN points out that 'self' does not automatically cover WebSocket schemes in all browsers. There is no separate directive named stream-src.

CSP and CORS solve different problems. CSP controls where the page is allowed to connect in the first place; CORS dictates on the server side which origins can read a response in the browser. A CSP rule will therefore not resolve a CORS error or an expired access token. A same-origin proxy can simplify your policy, but it must still correctly handle authentication, rate limits, timeouts, and error forwarding.

3. Images, Avatars, and Generated Media

In img-src, allow only your own origin and the media origin actually in use. data: is only necessary if the widget relies on small embedded images; blob: is only needed if the browser constructs images as blob URLs. Every additional source expands your attack surface. If an image is fetched first via fetch() and then converted into a blob URL, both connect-src and img-src may be affected.

Do not test just the default avatar. Check preview images, source screenshots, file attachments, dark mode, and error state fallbacks for missing media. URL query parameters can leak sensitive information into CSP reports; reporting endpoints should process reports with data minimization in mind and avoid keeping them indefinitely.

4. iframe, Styles, Fonts, and Optional Workers

A widget embedded directly into the DOM usually does not need a third-party frame. In that case, frame-src 'none' can remain. If the chat runs inside an iframe instead, restrict access exclusively to its exact origin. This is distinct from frame-ancestors: that directive is set on the served resource to control which pages are allowed to embed it. The widget provider must configure it appropriately on their iframe response.

The same principle applies to styles and fonts. Specify exact hosts and avoid 'unsafe-inline' whenever the integration permits. Workers or audio capabilities should only be added if the product actually utilizes them. Adding blob:, broad wildcard domains, or arbitrary media sources "just in case" makes future security audits significantly harder.

A Realistic CSP Example for a Chatbot Widget

The following domains are deliberately reserved example domains. Replace them with the origins discovered during your network analysis. This example assumes an external loader, an HTTPS API, a separate WebSocket for streaming, and a media host. It avoids broad wildcards:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.chat.example 'nonce-{RANDOM}';
  connect-src 'self' https://api.chat.example wss://stream.chat.example;
  img-src 'self' data: https://media.chat.example;
  style-src 'self' 'nonce-{RANDOM}';
  font-src 'self';
  frame-src 'none';
  worker-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self';
  form-action 'self';
  upgrade-insecure-requests;

{RANDOM} represents a strong value generated fresh for each response, identical in both the header and the allowed script or style elements. If your widget uses an iframe, replace frame-src 'none' with the exact widget origin. If it relies exclusively on HTTPS streaming via fetch() or EventSource, omit the WebSocket origin. Remove any source that proves unnecessary after thorough functional testing.

This policy serves as a practical starting point, not a one-size-fits-all template. A modern strict CSP can control scripts even more tightly using nonces or hashes combined with 'strict-dynamic'. Whether this works without compatibility issues depends on how the loader loads additional scripts. Clarify this behavior with your provider and test across browsers, consent modes, and deployment variants.

Moving from Report-Only to an Enforced Policy

Do not enforce a new policy without prior testing. The W3C Content-Security-Policy-Report-Only mechanism reports violations without blocking resources. This helps you identify missing image hosts, mismatched streaming origins, or inline code before users are affected. OWASP recommends using the HTTP header as the preferred delivery method; unlike meta tags, it supports the full feature set of CSP.

  1. Create an inventory: Test widget startup, initial message, long streaming response, sources, images, feedback, handoff, and consent state changes across multiple page types.
  2. Deploy Report-Only: Start with your planned tight policy and gather violation reports over a defined timeframe. Filter out browser extensions and non-reproducible noise.
  3. Justify every host: Add to the policy only if a concrete product feature relies on that origin. Avoid adding wildcards in response to isolated reports.
  4. Automate testing: Introduce end-to-end tests that send a message, wait for streaming to complete, and render an image while asserting that no CSP errors appear in the browser console.
  5. Enforce and monitor: Switch on the Content-Security-Policy header, keep a stricter Report-Only variant running in parallel for observation, and monitor error rates.

A phased rollout pairs well with running your chatbot in shadow mode. For streaming-specific metrics, consult our guide on latency budgets and timeouts. CSP violations should be tracked as a distinct metric: a timeout and a blocked connection require completely different root-cause analyses.

Common Misconfigurations

  • Overly broad source lists: Using *, https:, or large wildcard domains makes the policy easy to maintain, but weak and difficult to audit.
  • Testing only the initial view: The widget loads fine, but streaming, feedback, images, or human handoff fail later during interaction.
  • Leaving 'unsafe-inline' permanently: A temporary workaround is never replaced with proper nonces, hashes, or external script files.
  • Confusing CSP with access control: The policy does not replace server-side authorization checks, session validation, or protection against unauthorized tool calls.
  • Exposing sensitive data in reports: Full URLs, query parameters, or user contexts accumulate unnecessarily in monitoring systems.
  • Staging and production drift: Differing CDN, API, or WebSocket hosts only surface after going live.

Even a strict script-src does not automatically render an allowed third party safe: its JavaScript executes with the full permissions your site grants it. Treat provider changes, new subdomains, and loader updates as security-sensitive dependency updates. Our article on prompt injection defense complements this browser-level boundary with controls for RAG, tools, and data handling.

Pre-Go-Live Checklist

  • Have all required origins been documented and justified using real browser sessions?
  • Does script-src restrict execution to the loader and verified scripts, without relying on 'unsafe-inline'?
  • Does connect-src list the exact HTTPS, EventSource, and optional WSS origins required?
  • Are image, style, font, frame, and worker origins defined separately and kept as narrow as possible?
  • Are nonces freshly generated per response and attached strictly to trusted elements?
  • Have consent switches, long streams, images, errors, handoffs, and mobile devices been thoroughly tested?
  • Was the policy monitored in Report-Only mode before being enforced via HTTP header?
  • Are CSP reports sanitized to avoid collecting sensitive URL parameters or PII?
  • Is an automated regression test in place for future widget or infrastructure updates?

Conclusion

A solid CSP for website chatbots is not a haphazard collection of exceptions, but a clean blueprint of permitted browser connections. By isolating loader, API, streaming, image, and iframe resources, specifying exact origins, and initiating deployment in Report-Only mode, you keep your widget fully operational while depriving unexpected scripts and connections of room to execute.

References

Turn website visits into better conversations

Launch an AI chatbot that is useful from day one

Train ChatReact with your website, documents, and approved facts so visitors get faster answers and your team gets fewer repetitive requests.

Related articles

Keep reading