UpVoiced Docs · Widget
Strict Content Security Policy
The widget works under a strict policy without 'unsafe-inline' or 'unsafe-eval'. It runs no eval, writes no inline style attributes, renders text only (no HTML injection, no images from posts) and keeps its styles in its own Shadow DOM.
The directives the widget needs
| Directive | Why |
|---|---|
script-src: your nonce (or https://cdn.upvoiced.com) | w.js, and the panel script it adds the first time someone opens the widget. w.js gives that script your nonce. |
connect-src https://api.upvoiced.com | Your workspace’s boards, votes, sign-in and unread state. |
frame-src https://cdn.upvoiced.com | The short security check (Cloudflare Turnstile, P2-030) shown only when someone signs in or subscribes many times from one network. It runs on our CDN, inside a frame. |
style-src: your nonce | Only for browsers without constructable stylesheets: the widget then adds one <style> inside its Shadow DOM, carrying your nonce. |
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}';
style-src 'self' 'nonce-{RANDOM}';
connect-src 'self' https://api.upvoiced.com;
frame-src https://cdn.upvoiced.com;
img-src 'self' data:;
object-src 'none';
base-uri 'none'{RANDOM} is a fresh random value (at least 128 bits, base64) generated by your server for every response. Never reuse a nonce or hard-code it.
The tags
Put the nonce on both script tags and repeat it in data-nonce. w.js reads the nonce of its own tag and gives it to the panel script; data-nonce is the fallback where the nonce is not readable (some tag managers and older browsers).
<!--
Strict CSP: your server creates a fresh random nonce for every response and puts the same value
in the Content-Security-Policy header and on these two script tags. w.js passes it on to the
panel script it loads and to its fallback <style>, so no 'unsafe-inline' or 'unsafe-eval' is needed.
-->
<script nonce="{{CSP_NONCE}}">
window.UpVoiced = window.UpVoiced || { q: [] };
['identify', 'config', 'open', 'close', 'on', 'track'].forEach(function (m) {
window.UpVoiced[m] =
window.UpVoiced[m] ||
function () {
window.UpVoiced.q.push([m].concat([].slice.call(arguments)));
};
});
UpVoiced.on('ready', function () {
document.documentElement.setAttribute('data-feedback-ready', 'yes');
});
</script>
<script
async
nonce="{{CSP_NONCE}}"
src="https://cdn.upvoiced.com/w.js"
data-workspace="your-slug"
data-nonce="{{CSP_NONCE}}"
></script>
// Express: one nonce per response, in the header and in the HTML.
import { randomBytes } from 'node:crypto';
app.use((req, res, next) => {
res.locals.cspNonce = randomBytes(16).toString('base64');
res.set(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${res.locals.cspNonce}'`,
`style-src 'self' 'nonce-${res.locals.cspNonce}'`,
"connect-src 'self' https://api.upvoiced.com",
"frame-src https://cdn.upvoiced.com",
"img-src 'self' data:",
"object-src 'none'",
"base-uri 'none'",
].join('; '),
);
next();
});
// In the template, replace {{CSP_NONCE}} with res.locals.cspNonce.Other policy styles
'strict-dynamic'policies trust scripts that a nonced script adds, so the panel loads either way:script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline'- Host allowlists without nonces work too; keep the style nonce for the rare fallback:
script-src 'self' https://cdn.upvoiced.com; style-src 'self' 'nonce-{RANDOM}'; connect-src 'self' https://api.upvoiced.com; frame-src https://cdn.upvoiced.com - Next.js with a nonce from middleware: pass it as
nonceanddata-nonceonnext/script. SvelteKit withkit.csp: use%sveltekit.nonce%inapp.html(snippet).
Check it
Open the widget once and switch tabs, then look at the browser console: a missing directive shows as a Refused to load or Refused to connect CSP report naming it. A connect-src error for api.upvoiced.com is the most common one.