CSP, iframes and security requirements
The Screeb tag is a third-party script that runs inside your page. If your site enforces a Content Security Policy, is served inside an iframe, or restricts browser permissions, you need to grant Screeb a small, explicit set of privileges.
This page lists exactly what the tag needs, what happens when a requirement is missing, and which features keep working anyway.
Screeb never requires 'unsafe-eval'. The tag contains no eval, no
new Function, and no WebAssembly, so a policy that forbids dynamic code
evaluation is fully supported.
Content Security Policyβ
The short versionβ
This policy runs every Screeb feature:
default-src 'self';
script-src 'self' https://*.screeb.app;
connect-src 'self' https://*.screeb.app wss://*.screeb.app https://*.s3.fr-par.scw.cloud;
style-src 'self' https://*.screeb.app;
img-src 'self' data: blob: https://*.screeb.app;
font-src 'self' https://*.screeb.app;
media-src 'self' blob: https://*.screeb.app;
frame-src 'self';
No Screeb origin appears in frame-src: the frames the tag creates carry no
src. Add your booking provider's origin there if you use booking questions.
That is the whole policy β no 'unsafe-inline' anywhere, for scripts or for
styles, provided you install the tag without an inline snippet. See
Installing without an inline script.
If you route Screeb through your own domain (see
Custom Collector URL), replace *.screeb.app with
your proxy hostname in every directive above.
Directives Screeb does not needβ
You can leave these as strict as you like:
| Directive | Why it is not needed |
|---|---|
'unsafe-eval' | No eval, no new Function, no WebAssembly. Browser capability detection is feature-based. |
script-src 'unsafe-inline' | Only the optional HTML snippet needs it. The attribute install below has no inline script; neither does the npm SDK. |
style-src-attr 'unsafe-inline' | The tag writes no style attribute. Everything it styles at runtime goes through CSSOM or its own stylesheet, neither of which CSP governs. |
worker-src | The tag starts no Web Worker, Shared Worker or Service Worker. |
object-src | No object, embed or applet elements. |
base-uri, form-action | The tag renders no form and never rewrites base. |
prefetch-src | The tag issues no prefetch or preload link. Older Screeb documentation asked for this; it is obsolete. |
font-src blob: | Fonts moved off blob URLs in tag 0.48.1 and are now loaded by URL through the Font Loading API. Also obsolete. |
Where Screeb loads fromβ
| Origin | Used for | Directives |
|---|---|---|
https://t.screeb.app | Loader tag.js, the versioned feature bundles (core.js, widget.js, editor.js, inspector.js), their stylesheets, webfonts, UI translations, notification sounds, recoloured emoji | script-src, style-src, font-src, connect-src |
https://rpc.screeb.app | REST API: survey configuration, identity, event capture, session-replay batches, upload authorisation | connect-src |
wss://centipede.screeb.app | Real-time channel carrying survey questions and answers | connect-src |
https://r.screeb.app | SDK error and health reporting | connect-src |
https://static.screeb.app | Images, audio and video attached to your questions, and reading back uploaded answer files | img-src, media-src |
https://emoji.screeb.app | Emoji glyphs (SVG) | img-src |
https://*.s3.fr-par.scw.cloud | Pre-signed upload of file, voice, video and screenshot answers | connect-src |
https://admin.screeb.app | postMessage peer for the survey builder only β never fetched from your page | none |
All Screeb origins are HTTPS/WSS only. The tag refuses any non-HTTPS custom
endpoint, so a policy containing upgrade-insecure-requests or
block-all-mixed-content changes nothing.
What each directive controlsβ
script-src β mandatoryβ
The loader and the feature bundles are ES modules served from the tag origin.
The bundles run inside frames the tag creates on your page; those frames have no
src, so they inherit your page's CSP β your script-src governs them too.
If missing: nothing loads. The tag reports the failure and every pending
$screeb() call rejects quickly instead of hanging. Your page is otherwise
unaffected.
connect-src β mandatoryβ
Covers the REST API, the report endpoint, the WebSocket channel, and fetches of the tag's own translations, sounds and stylesheet text.
wss: is a distinct scheme: connect-src https://*.screeb.app does not
authorise wss://centipede.screeb.app. List both.
If missing: see the matrix β the REST endpoint and the WebSocket have very different blast radii.
style-srcβ
Widget, editor and inspector CSS ship as real stylesheets from the tag origin.
The tag sets no style attribute anywhere β anything it styles at runtime
(recoloured emoji, widget geometry) is applied through CSSOM, which CSP does not
govern β so style-src-attr 'unsafe-inline' is never needed.
If missing: the tag degrades in three steps rather than rendering an unstyled panel in front of your users:
- A
stylesheetlink β the normal path. - Constructed stylesheets (
adoptedStyleSheets) β CSSOM, whichstyle-srcdoes not govern, so this works even understyle-src 'none'. Requires Chrome 73+, Firefox 101+, Safari 16.4+. - An inline
styleelement β for the opposite policy, one that allows'unsafe-inline'but not the Screeb origin.
Every fallback is logged as an error, because only you can fix the policy. If all three fail β an old browser under a policy that refuses both the origin and inline styles β no survey, in-app message or feedback button renders at all, while tracking and targeting keep working normally. This is the CSP mistake with the least visible symptoms; the health inspector ranks it first for that reason.
img-srcβ
Question images and logos (static.screeb.app), emoji (emoji.screeb.app),
recoloured emoji (t.screeb.app), data: for the transparent placeholder a
masked emoji is painted over, and blob: for the thumbnail of a file or
screenshot a respondent has just attached.
If missing: broken images. Text, navigation and answer submission are unaffected.
font-srcβ
Survey webfonts (Inter, Rubik, Montserrat, your custom brand fontβ¦) are declared
through the Font Loading API against t.screeb.app/assets/fonts.
If missing: the widget falls back to the system font stack. Everything else works.
media-srcβ
Audio and video attached to your questions (static.screeb.app), plus blob:
for two local-only cases: playing back the voice or video answer a respondent
just recorded, and the notification sound.
If missing: media does not play; the survey stays answerable.
frame-srcβ
Needed only for booking questions, which embed your scheduling tool (Calendly, Microsoft Bookings, HubSpot Meetingsβ¦) in an iframe. You must add that provider's origin yourself β it is not a Screeb domain, and the tag only ever loads the URL you configured on the question (over HTTPS; anything else is refused before it reaches the frame).
No Screeb origin belongs here. The frames the tag creates for its own bundles
carry no src at all β they inherit your page's origin and policy, so
frame-src 'self' covers them.
If missing: the booking question renders an empty box and cannot be completed. Other questions are unaffected.
Installing without an inline scriptβ
The classic HTML snippet is an inline <script>, which script-src blocks
unless you allow 'unsafe-inline'. Four ways around it, best first:
1. Declare the channel id on the script tag. No inline script at all:
<script
async
id="$screeb"
src="https://t.screeb.app/tag.js"
data-channel-id="<website-id>"
></script>
The tag reads data-channel-id off its own element and initialises itself once
it is ready. Keep id="$screeb" β the tag uses that element to work out which
origin it was served from.
This install carries the channel id and nothing else.
If you also call $screeb('identity', β¦), $screeb('track', β¦) or pass init
options, you still need JavaScript β and it must define the small queue stub
first. On its own the script tag does not create window.$screeb until the tag
has finished loading, so calling it directly races the download and throws
$screeb is not a function. The stub queues your calls until the tag is ready:
// `typeof`, not `||`: the loader tag's id="$screeb" makes window.$screeb the
// script element itself until the tag finishes loading, and an element is
// truthy β so `||` would keep it and calling it throws.
if (typeof window.$screeb !== "function") {
window.$screeb = function () {
var args = arguments;
return new Promise(function (ok, ko) {
(window.$screeb.q = window.$screeb.q || []).push({ v: 1, args: args, ok: ok, ko: ko });
});
};
}
$screeb('identity', 'user-id', { email: 'support@screeb.app' });
That file is a normal script, so script-src 'self' covers it β no inline
allowance needed. Do not combine the attribute with your own init call: if the
page calls init itself, that call wins and the attribute is ignored.
2. Use the npm package β @screeb/sdk-browser (or
the React, Angular, Vue, Svelte or Ionic wrapper). Your bundler emits a normal
file covered by script-src 'self'.
3. Nonce β keep the snippet and add your per-response nonce to it. The
loader injects its own scripts with a src, so no nonce propagation is needed:
<script nonce="YOUR_REQUEST_NONCE">
/* Screeb snippet */
</script>
4. Hash β compute the SHA-256 of the snippet body and add
script-src 'sha256-β¦'. Remember to recompute it whenever you change the init
options.
https://t.screeb.app/tag.js is a mutable pointer updated on every release, and
the loader fetches versioned bundles at runtime without integrity attributes.
Adding an integrity attribute to the snippet will break the tag on the next
release. Use script-src allowlisting and require-trusted-types-for for
supply-chain control instead.
Running inside an iframeβ
If your page is embedded in another document β a portal, a marketplace
shell, a partner site, a native webview β that embedder's sandbox attribute
and permission policy apply to Screeb too.
sandbox tokensβ
| Token | Needed for | Without it |
|---|---|---|
allow-scripts | Everything | The tag cannot run at all. |
allow-same-origin | Everything | The frame gets an opaque origin: localStorage throws and anonymous mode refuses to initialise (see Storage requirements). The tag also cannot read the host DOM, so URL, scroll and element targeting stop. |
allow-popups and allow-popups-to-escape-sandbox | CTA buttons and links that open in a new tab | window.open returns null; the CTA does nothing. |
allow-top-navigation-by-user-activation | CTA buttons that navigate the current tab | The tag first tries window.top.location, then falls back to an anchor with target="_top", which this token permits. Without either, the navigation is silently blocked. |
allow-forms, allow-modals and allow-downloads are not required: the
tag renders no form, never calls alert/confirm/prompt, and triggers no
download.
Permissions Policy (the allow attribute)β
Frames the tag creates on your page are same-origin, so they inherit your page's permissions automatically β you do not need to annotate them. But if your page is framed, its embedder must delegate:
<iframe
src="https://app.example.com"
allow="microphone; camera; display-capture; fullscreen; clipboard-write"
></iframe>
| Permission | Screeb feature | Without it |
|---|---|---|
microphone | Voice answers on open-text questions | The record button fails; typing still works. |
camera | Video answers on open-text questions | Same β the other answer modes still work. |
display-capture | Screenshot capture on open-text questions | Capture fails; file upload still works. |
fullscreen | Fullscreen button on video questions | Video plays inline only. |
clipboard-write | "Copy to clipboard" contextual actions | The action silently does nothing. |
autoplay (optional) | Auto-starting video in a question | The respondent presses play manually. |
Cross-origin framesβ
When Screeb runs in one origin and the content you target lives in another, the browser blocks all access between them and several targeting rules stop resolving. That is a separate topic with its own page: Cross-origin pages and iframes.
Cross-Origin-Opener-Policy and the survey builderβ
The visual survey builder opens your site in a popup and talks to it through
window.opener and window.name.
A page that sends Cross-Origin-Opener-Policy: same-origin triggers a
browsing-context-group switch that severs that relationship: the builder sees
the popup as closed, window.opener is null, and window.name is reset. This
is mandated by the specification and cannot be worked around from Screeb's side.
Workarounds, in order of preference:
- Use the builder's "Open with URL parameters" action. It bootstraps the
editor through the page URL and syncs over the API instead of
postMessage, so it is immune to COOP. The builder then reflects your changes on refetch rather than live. - Relax the header to
Cross-Origin-Opener-Policy: restrict-propertiesorsame-origin-allow-popupson the pages you edit surveys on.
Cross-Origin-Embedder-Policy: require-corp is not compatible with the tag:
Screeb's assets are not served with Cross-Origin-Resource-Policy: cross-origin.
frame-ancestors is not involved β the builder uses a popup, never an
iframe.
Other security requirementsβ
Storage requirementsβ
Screeb sets no cookies, in your page or anywhere else. Identity and display
history live in localStorage and sessionStorage on your own origin, so no
third-party-cookie or Storage Access permission is involved and browser
anti-tracking modes do not affect the tag.
| Situation | Behaviour |
|---|---|
| Storage available | Normal operation. |
Storage blocked or full, user identified via $screeb('identity', β¦) | Falls back to an in-memory store. The session works, but display history and quotas reset on every page load, so a respondent may see the same survey again. |
| Storage blocked, anonymous mode | init fails with an explicit error. Anonymous mode needs a persistent identifier β without one, every page load would be a new respondent. |
Network and transportβ
- All endpoints are HTTPS/WSS. The tag rejects a non-HTTPS custom endpoint at initialisation.
- API requests are sent in CORS mode and without credentials β no cookies
and no
Authorizationheader from your session are attached. - Requests carry the browser's
Originheader, which is what the allowed-domains check below reads.Referrer-Policydoes not affect it.
Allowed domainsβ
Workspaces on a plan with Custom Domains can restrict which origins may talk
to the API (Screeb Admin β Settings β Custom Domains). Requests whose Origin
is not on the list are rejected with 403 origin not allowed. localhost,
127.0.0.1 and ::1 are always accepted, and requests with no Origin header
(mobile SDKs, server-to-server) are not checked.
If you also override the SDK endpoints, the tag validates them against the same list at startup and logs a console warning on mismatch.
Frame messaging trust modelβ
The tag communicates between the host page and its own frames with
postMessage. Incoming messages are accepted only from three origins: your own
page, https://t.screeb.app, and https://admin.screeb.app. Everything else is
dropped before it is parsed.
Being one of those origins is not enough on its own: each is also bound to the identity it is allowed to claim, so a message from one trusted origin cannot impersonate another part of the tag to reach handlers it should not. The loader always addresses its frames with its own origin, and never trusts an origin supplied inside a message.
Ad blockers and proxyingβ
Ad blockers commonly block *.screeb.app regardless of your CSP β the symptom
looks identical to a script-src failure. Enterprise plans can serve the tag
and its API from your own domain; see
Custom Collector URL and
Reverse proxy templates. Remember to update your
CSP to the proxied origins when you do.
Feature requirement matrixβ
Degrades answers one question: if the requirement is not met, does the rest of Screeb keep working?
| Feature | Requirement | If not met | Degrades? |
|---|---|---|---|
| Tag loads at all | script-src https://*.screeb.app | Nothing loads; $screeb() calls reject with a reported error | β Total β but your page is unaffected |
| HTML snippet install | script-src 'unsafe-inline', a nonce, or a hash | The snippet never executes | β Total β switch to the data-channel-id install, which needs none of them |
| Survey configuration, targeting, event tracking, identity | connect-src https://*.screeb.app | No configuration is fetched; nothing is captured | β Total |
| Answering a survey or in-app message | connect-src wss://*.screeb.app | The widget displays, then fails to submit once its reconnect budget runs out | β The survey is unusable |
| Survey / message rendering | style-src https://*.screeb.app | Falls back to constructed stylesheets, then to an inline style element | β οΈ Yes on modern browsers. β On older browsers under style-src 'none': nothing renders while tracking keeps working |
| Brand webfont | font-src https://*.screeb.app | The system font stack is used | β Cosmetic only |
| Question images, logos, emoji | img-src https://*.screeb.app data: | Broken images | β The survey stays answerable |
| Coloured / keycap emoji | style-src https://*.screeb.app β the same entry as the widget stylesheet | Falls back with the rest of the stylesheet | β Needs no directive of its own |
| Audio and video in questions | media-src https://*.screeb.app | Media does not play | β Other questions unaffected |
| Fullscreen video | allow="fullscreen" when your page is framed | Inline playback only | β |
| Notification sound | media-src blob: | Silent | β |
| Voice answers | allow="microphone" when your page is framed | Recording fails | β Typed answers still work |
| Video answers | allow="camera" when your page is framed | Recording fails | β |
| Screenshot answers | allow="display-capture" when your page is framed | Capture fails | β File upload still works |
| Playing back a recorded answer before sending | media-src blob: | The preview does not play | β The answer still uploads |
| Attachment thumbnails | img-src blob: | No preview thumbnail | β The upload still works |
| Uploading file / voice / video / screenshot answers | connect-src https://*.s3.fr-par.scw.cloud | The upload fails | β The rest of the survey completes |
| Booking questions | frame-src + your booking provider's origin | Empty embed | β For that question only |
| "Copy to clipboard" actions | allow="clipboard-write" when your page is framed | Nothing is copied | β |
| CTA opening a new tab | sandbox allow-popups allow-popups-to-escape-sandbox | The CTA does nothing | β |
| CTA navigating the current tab | sandbox allow-top-navigation-by-user-activation | Navigation is blocked | β |
| Session replay | connect-src https://*.screeb.app and a readable same-origin host page | Recording does not start | β Surveys are unaffected |
| Anonymous respondents | localStorage writable, sandbox allow-same-origin | init fails with an explicit error | β Identify your users instead |
| Identified respondents | none β falls back to in-memory storage | Display history and quotas reset each page load | β |
| URL, scroll, element and exit-intent targeting | Same-origin access to the host page | The rule never matches | β οΈ Time, event and manual triggers still work |
| Survey builder on your site | No Cross-Origin-Opener-Policy: same-origin | Popup detection and live sync are severed | β οΈ Use "Open with URL parameters" |
| SDK error reporting | connect-src https://r.screeb.app | Screeb support loses diagnostics | β Invisible to respondents |
Checking your setupβ
Run this in your browser console on a page where Screeb is installed:
$screeb('debug');
The integration health report probes each directive live β API reachability, the WebSocket channel, the widget stylesheet, fonts and media β and names the directive to fix for each failure.
CSP violations caused by Screeb are also logged to the console with a link to this documentation. To catch them yourself:
document.addEventListener('securitypolicyviolation', (e) => {
console.log(e.violatedDirective, e.blockedURI);
});
If something still does not add up, the Troubleshooting page covers the non-CSP causes.