Chat Widget Under a Strict Content Security Policy: Frame, Integrity, Allowlists
What an embedded chat window asks of a strict Content Security Policy, how the frame and integrity hashes protect, and how to roll out the policy without breakage.
In plain language
Germany's security office BSI advises a firm rule for scripts and frames. This rule is called a Content Security Policy. A chat window has to fit into it. For our window your rule needs two entries: one for the script and one for the frame. Both use the address from the embed code. The window runs in a frame that only allows scripts. Try a stricter rule first in a mode that only reports. You decide which pages may load the assistant.
Before the Christmas trading season, many websites go quiet: from a set date no further changes go live, and whatever is not in place by then waits until the new year. In the weeks before, many businesses therefore tighten their security headers, first and foremost the Content Security Policy. An embedded chat window is a typical point of conflict: it puts script on the page, opens a frame and talks to a server. This article shows what a widget can ask of a policy, which two entries your policy needs for the XICBOT loader and its chat window according to the integration page and which directives stay untouched, how the frame is sealed off and delivery is secured with integrity checks, how you decide which pages may load the assistant, and what the embed does not include.
Labels in the illustration
- Your page: Content-Security-Policy
- script-src
- allow the loader's source
- connect-src
- not needed for the loader
- style-src
- no stylesheet attached
- img-src
- brand built from elements
- frame-src
- allow the frame's address
- after page load
- Frame with its own origin
- sandbox with exactly one permission: scripts
- the origin reports itself as null
- allow-same-origin left out on purpose
- together with scripts it would undo the sandbox
- Both parts with integrity and crossorigin
- snippet without integrity: entry name changes
- Messages carry commands, not the history
- start data in the URL fragment, not on a server
- Reverse direction: which pages may load it (domain list per assistant)
- Host on the list
- widget loads
- Host not on the list
- widget does not load
- List empty
- every host is allowed
Why security headers get tightened before the season
A code freeze before the Christmas season has a simple reason: in the weeks with the highest sales, no change should put the site at risk. A second rule follows from it that is stated less often: whatever still needs to change in delivery is changed beforehand. Headers such as the Content Security Policy belong to this, because they change the behaviour of every page at once. A policy that is set too tight in November can break a payment step, a form or an embedded chat window without anyone noticing in their own test. A policy that stays too loose, on the other hand, protects very little. For the question of how tight it should be there is a sober reference point: the IT-Grundschutz Compendium of Germany's Federal Office for Information Security (BSI). In the module for web applications and web services, requirement APP.3.1.A21 reads:
To protect against clickjacking, cross-site scripting and other attacks, IT operations SHOULD set suitable HTTP response headers. At a minimum, the following HTTP headers SHOULD be used: Content-Security-Policy, Strict-Transport-Security, Content-Type, X-Content-Type-Options and Cache-Control.
Directly after the list comes the sentence that matters for a chat window: "The HTTP headers used SHOULD be as restrictive as possible." Within the module, the requirement is one of the standard requirements. The Compendium says of them that, together with the basic requirements, they correspond to the state of the art for this module and should in principle be met. As restrictive as possible means, for a Content Security Policy: every source a page may load is listed individually, and every exception has a reason. A widget that is embedded without knowing what it loads sits badly with that. A widget whose needs are documented, by contrast, can be placed line by line.
What a chat window typically asks of a policy
A Content Security Policy is a list of directives, and each one governs one kind of resource: scripts, frames, connections, styles, images. Depending on how it is built, an embedded chat window touches several of them at once, because it consists of several parts: a script line in your template, a visible launcher, a window that often runs in a frame, and a connection to a server that delivers the answers. The table describes in general terms what a widget typically needs a directive for. It does not describe a particular product; which rows apply to a specific widget is, at best, stated in its documentation.
| Directive | What a widget typically needs it for | Question for the vendor |
|---|---|---|
| script-src | The script line in your template loads a file from a particular address; some widgets load further scripts afterwards. | From which addresses does the widget load scripts, and does it load more later? |
| frame-src | If the window runs in a frame, a policy that restricts frames has to allow its address. | Which address does the frame come from, and is that documented? |
| connect-src | If the script on your page calls an API itself, it needs permission for that address. | Does the part on my page make its own network requests? |
| style-src | If a widget attaches stylesheets or style elements to your page, it needs permission for them, in the worst case unsafe-inline. | Does the widget attach styles to my page, and in what form? |
| img-src | Logos, avatars and launcher images that arrive over the network or as data URLs need a matching image source. | Where do the widget's images come from? |
The conflict rarely comes from bad intent, but from convenience. If a widget's documentation only says that the policy needs adjusting, permissions tend to be granted generously: a wildcard for all subdomains, unsafe-inline for styles, connections to any address. Each of these exceptions then applies not only to the chat window but to everything that runs on the page. Step by step the policy grows looser, without anyone having decided so. Attacks through content that an assistant reads are a separate topic, covered in the article on prompt injection. This article is about the embedding itself.
What the XICBOT loader does not need
For XICBOT, the integration page describes which directives your policy does not need to loosen for the loader script, and gives the reason from the source code for each. What gets embedded is a single script element with the defer attribute and the public key of your assistant. The loader is a single file without dependencies; it opens a wrapper with an open shadow DOM on your page, draws the launcher inside it and attaches the chat window only after your page has fully loaded. The wrapper's properties are set individually on the element so that your policy has nothing to block. Three directives therefore stay untouched:
- No connect-src for the API: the loader itself fetches nothing over the network; traffic only starts inside the frame.
- No style-src: no stylesheet and no style element is attached, and assignments on the element do not fall under this directive.
- No img-src for the brand mark: the mark in the launcher is built from elements, not from an image.
- A custom launcher image arrives as a data URL rather than over the network; SVG is rejected there because SVG can carry script.
What runs on your page therefore stays manageable. In our own measurement the loader on your page, compressed, came to 24.1 KB (own measurement of the delivered files). Only the file that runs on the host page was measured, with the compression the web server actually sends; the chat window lives in its own frame and is not included in this figure. What that means for load time is described in the article on chat widgets and load time; the measurement itself is listed under measurements.
Two delivery routes, one separate origin
The loader is delivered by one of two routes: through the widget domain or through a path on your own domain that you set up. The loader determines its base from the directory it was loaded from; if that base is on your domain, the API runs under it as well. For your page's policy the difference is tangible: with the widget domain, the visitor's network log shows the widget address next to yours; with your own path, only your address. In both cases the chat window brings its own policy; the two headers for it are set either by the edge at the widget origin or by your server on your path.
| Aspect | Through the widget domain | Through a path on your domain |
|---|---|---|
| The snippet | carries the API address next to the key | carries your path instead of the API address |
| Prerequisite | none, the line is enough | three blocks in your server configuration: entry point, version names, API |
| In the visitor's network log | your address and the widget address | only your address |
| Headers for the frame's policy | set by the edge at the widget origin | set by your server on your path |
| The chat window | runs in its own origin | runs in its own origin here too |
The last row is the decisive one. First-party means two things here, and the two levels are often confused: you can move delivery onto your domain, yet the chat window still runs in its own origin, because that is exactly where the separation between your page and the conversation lies. Which route suits you depends mainly on who looks after your server. How the embedding works overall is described in the article Embed an AI assistant without slowing your site; where the data is processed afterwards is set out under privacy and hosting.
The frame as a barrier: one permission, not two
A frame with the sandbox attribute starts with every restriction in place: its content is treated as coming from a separate, opaque origin, and scripts, forms and a number of other interfaces are disabled. Each permission listed in the attribute lifts one of these restrictions again. The XICBOT chat window runs in a frame with exactly one permission: scripts. The same-origin permission, allow-same-origin, is left out on purpose. The frame's origin therefore reports itself as null, and the separation works in both directions: your page cannot read the window, and the window cannot read your page. Why this second permission in particular is missing is stated as a warning in the HTML specification:
Setting both the allow-scripts and allow-same-origin keywords together when the embedded page has the same origin as the page containing the iframe allows the embedded page to simply remove the sandbox attribute and then reload itself, effectively breaking out of the sandbox altogether.
The sentence describes exactly the case that arises when you deliver through your own path: frame and page then share the same origin. With both permissions, the sandbox would be ineffective in that case. Without the second one, the frame remains a separate, opaque origin even when it is loaded from your domain. The features overview states it in one sentence: the chat window runs in its own origin without allow-same-origin. For anyone working on the policy and the template, this yields a rule worth writing down: whoever touches the frame later does not add the same-origin permission, not even to fix a display issue quickly. What is still exchanged between page and window, and what of it stays on your page:
- Page and window communicate through a message protocol carrying commands and IDs, not the conversation history. Form values from the conversation only reach your page after the visitor has confirmed them.
- The loader stores the conversation ID in your page's storage with an expiry. Anyone who knows it can retrieve the history; more on this below under the limits.
- The frame does not pass a referrer address to the outside.
- The start parameters sit in the URL fragment; a fragment is not sent to a server and does not appear in any access log.
- The loader re-checks geometry messages from the window field by field, because the route between them passes through a foreign page.
- The loader reads exactly three values from your page, font, corner radius and brand colour; the frame cannot inherit anything.
Integrity checks: where they sit and where they do not
An integrity hash in the integrity attribute tells the browser which content it expects for a file. If the delivered file does not match, it is discarded. The specification for this, Subresource Integrity, is available from the W3C as a working draft dated 20 March 2026 and describes the consequence of a failure as follows:
The user agent will refuse to render or execute responses that fail an integrity check, instead returning a network error […]
With XICBOT, the two parts the frame loads carry such a hash, together with version names derived from their content and the crossorigin attribute stating that they are loaded from a foreign origin. This attribute is mandatory, not decoration: without it the response would be opaque and the check would fail. A version name never holds any content other than the one the name was derived from; an unknown version name produces an error, not a silent substitute. A version name is cached for a year, the entry name for five minutes, the frame shell not at all.
Why the snippet carries no integrity hash
The embed snippet deliberately carries no integrity hash. It would sit on the entry name, whose content changes with every version, and would lock out every embedding page with the next delivery. The commitment sits one level lower, on the two parts of the window; the admin area shows the hash of the delivered file and states the reason if the file cannot be read.
Delivery also includes the headers of the widget files: caching directives, an open origin permission and an explicit resource permission for foreign origins. After every rollout a smoke test runs that counts these headers instead of merely looking at them, and checks whether the version files inside the frame carry their integrity hash. For your template this means: the line is inserted once and not touched again, even when a new version of the widget is delivered. How a version change of the assistant itself works is described in the article on updating without downtime.
The reverse direction: which pages may load it
A Content Security Policy protects your page from what it loads. It does not ask the reverse question: who may embed your assistant on their page? The public key appears in the source of every page that loads the widget and can be copied. For this, XICBOT has a domain list for each assistant. It decides which hosts may load; on a page that is not on the list, the widget does not load. The list is maintained in the application rather than in a web server configuration, because it differs by account and by assistant. If, for example, a careers page runs on its own domain, that domain belongs on the list of the responsible assistant; how assistants can be divided up within one account is described in the article on several assistants in one account.
Rolling out without breakage: report first, then enforce
A stricter policy is safest when it is not switched on all at once. For this there is a second header, Content-Security-Policy-Report-Only. It does not enforce the policy but only reports what it would have blocked, in the browser console and, if you wish, to a reporting address. The specification Content Security Policy Level 3, a W3C working draft dated 16 September 2026, describes this path explicitly:
This header field allows developers to piece together their security policy in an iterative fashion, deploying a report-only policy based on their best estimate of how their site behaves, watching for violation reports, and then moving to an enforced policy once they’ve gained confidence in that behavior.
- Set the planned policy as Content-Security-Policy-Report-Only on a test environment that has the widget embedded and whose domain is on the domain list. This header is not supported as a meta element; it belongs in the server response.
- Load the page, press the launcher, hold a conversation and read the console meanwhile: reported violations usually name the directive and the blocked address.
- Decide on each reported violation individually: allow the specific address or remove the cause, rather than opening up the directive wholesale.
- In the app, check under the assistant in the “Chat window on your website” section, under “Embedding on your website”, whether the widget has loaded on the test host; the time and page of the last load are shown there.
- Only then switch the same policy on as Content-Security-Policy and repeat the run on the live site.
The “Chat window on your website” section of the assistant in the app shows the snippet, a preview, the allowed domains and the embedding status in one place. The preview locks the input and does not count as an embed on the target page; for your policy, only a run on a page that actually serves it is meaningful. On request, a test assistant for your site is available beforehand. Which test cases belong together before go-live is described in the article on acceptance testing.
What the embed does not include
Honest planning includes the limits. The integration page lists them as acknowledged limits, taken from the source code. For work on a policy, these points matter most:
- Without JavaScript nothing appears. Wrapper, launcher and frame are created by the loader; there is no fallback.
- Only one widget per page. A second snippet with a different key returns silently and reports nothing.
- If your policy does not allow data URLs for images, a custom launcher image does not appear; the launcher then shows the brand dots.
- The host information is self-reported, and without a domain list every host is allowed.
- Anyone who knows the conversation ID can retrieve the history; there is no further secret. The ID sits in your page's storage with an expiry. Scripts there, including embedded third-party scripts, therefore have access to the history in principle.
- If the widget service fails, there is no fallback to another delivery route. This is deliberate, because a fallback would hide the outage.
Checklist before the code freeze
- Take stock: which security headers does your site set today, and is the Content Security Policy in the server response or in a meta element?
- For every embedded element, including the chat window, obtain the documented policy requirements and note any gaps as open questions.
- Decide on the delivery route, widget domain or your own path, and clarify who looks after the server configuration for it.
- Fill in the assistant's domain list before the line goes live; an empty list allows every host.
- Deliver the new policy in report-only mode first, open the widget, hold a conversation and evaluate the reported violations.
- Check in the browser's developer tools that the frame runs without allow-same-origin and that the parts inside the frame carry their integrity hash.
- Only then enforce it, with some distance to the code freeze date, and check the embedding on the live site again afterwards.
The most important point on this list is the distance. A policy that is enforced shortly before the deadline leaves no time to fix an overlooked violation without breaking the freeze. The two entries for the loader and the frame therefore belong in the version that runs in report-only mode; the technical details of the embedding are on the integration page.
Frequent questions
Do I have to loosen my Content Security Policy for the chat assistant?
No loosening, but two additions: script-src for the loader and frame-src for the chat window's frame, both with the address from the embed code. According to the integration page, connect-src, style-src and img-src for the brand mark need no entry. A custom launcher image arrives as a data URL and needs data: among the image sources.
Why does the embed snippet carry no integrity hash?
Because the content behind the entry name changes with every new version. A hash at that point would lock out every embedding page with the next delivery. The hashes sit one level lower, on the two parts the frame loads; there they appear together with version names derived from the content and the crossorigin attribute.
Can my website read what visitors type into the chat window?
Usually not directly from the window, but in principle yes, through the conversation ID. The chat window runs in a frame with its own origin and without the same-origin permission, so your page cannot read the content of the frame. The conversation ID, however, sits in your page's storage with an expiry, and anyone who knows it can retrieve the history; there is no further secret. Scripts on your page, including embedded third-party scripts, therefore have access to the history in principle. Between page and window themselves, commands and IDs pass, not the history; form values only reach your page after the visitor has confirmed them. What this means for your privacy notice is best clarified with your data protection officer.
What happens if I do not set up a domain list?
Then every host is allowed, and the widget loads on any page where someone inserts the snippet. It therefore makes sense to enter your own domains before the line goes live. Note that the host information is self-reported; against a copied embed, swapping the public key also helps.
How do I test a stricter policy without the chat breaking?
Typically with the Content-Security-Policy-Report-Only header. It reports violations without blocking anything. On a test environment with the widget embedded, you open the window, hold a conversation and read the console. Only when no unexpected reports appear any more is the same policy enforced.
Is the BSI requirement on security headers mandatory for my company?
That depends on the individual case. The IT-Grundschutz Compendium is not a law; requirement APP.3.1.A21 is one of the standard requirements that, according to the Compendium, should in principle be met. An obligation may arise, for example, if you work to IT-Grundschutz or a contract refers to it. In an individual case, this is best clarified with professional advice.
Ask it yourself, Assistant of this website
Automated assistant