CSP for Hyvor Talk Comments: A Real Before/After Fix
Table of Contents
I’ve seen this pattern a lot: a site has a reasonably tight Content Security Policy, then someone adds a third-party comments widget, and suddenly the console turns into a wall of CSP violations.
That’s exactly what happens with Hyvor Talk if you drop in the embed code and hope your existing policy will “just work”.
For a developer site like csp-guide, this is extra awkward. You want comments, but you also can’t publish a sloppy script-src * policy and call it a day.
Here’s a practical case study for adding Hyvor Talk comments to a site that already has a real CSP in production.
The starting point
A useful baseline is the CSP header seen on headertest.com:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-NzFhODk5NDUtZjNlYy00M2I2LThmZjktZWVhNjE3YWFkZWRk' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That’s not a toy policy. It already has:
- a nonce-based
script-src 'strict-dynamic'- a locked-down
object-src - explicit
connect-src - explicit
frame-src frame-ancestors 'none'
That’s the kind of CSP I like starting from, because it reflects reality: analytics, consent tooling, WebSocket endpoints, and a few carefully chosen exceptions.
Then we add Hyvor Talk comments.
The “before” integration that breaks
The default embed usually looks roughly like this:
<div id="hyvor-talk-view"></div>
<script type="text/javascript">
var HYVOR_TALK_WEBSITE = 1234;
var HYVOR_TALK_CONFIG = {
url: window.location.href,
id: location.pathname
};
</script>
<script async src="https://talk.hyvor.com/web-api/embed"></script>
On a site with the CSP above, this fails for predictable reasons.
What breaks
-
Inline config script is blocked The config block has no nonce, so
script-srcrejects it. -
The Hyvor Talk external script is blocked
https://talk.hyvor.comis not inscript-src. -
Requests back to Hyvor Talk APIs may be blocked If the widget makes API or websocket calls, they need to be allowed in
connect-src. -
Embedded frames may be blocked If Hyvor Talk uses iframes for parts of the UI,
frame-srcneeds to allow them. -
Images, avatars, or media may be blocked If comments load remote avatars or media,
img-srcmay need expansion.
The browser console usually tells the story pretty fast:
Refused to execute inline script because it violates the following Content Security Policy directive:
"script-src 'self' 'nonce-...' 'strict-dynamic' https://www.googletagmanager.com ..."
Refused to load the script 'https://talk.hyvor.com/web-api/embed' because it violates the following Content Security Policy directive:
"script-src 'self' 'nonce-...' 'strict-dynamic' ..."
Refused to connect to 'https://talk.hyvor.com/...' because it violates the following Content Security Policy directive:
"connect-src 'self' ..."
This is the good kind of failure. CSP is doing its job.
The wrong fix
I’ve also seen people “fix” this by doing something like:
Content-Security-Policy:
default-src * data: blob: 'unsafe-inline' 'unsafe-eval';
That’s not a fix. That’s removing the seatbelt because it was uncomfortable.
A more subtle bad fix is this:
script-src 'self' 'unsafe-inline' https://talk.hyvor.com;
That makes the widget work, but it weakens your policy in the exact place you probably care about most. If your site already uses nonces and 'strict-dynamic', adding 'unsafe-inline' is a big step backward.
The real fix
The right approach is to keep the existing CSP model and add only what Hyvor Talk actually needs.
There are two parts:
- make the inline config compatible with your nonce-based policy
- allow the specific Hyvor Talk origins required for scripts, connections, and frames
After: HTML with nonce
If your site already generates a nonce per request, use it for the Hyvor Talk config block too.
<div id="hyvor-talk-view"></div>
<script nonce="{{ .CSPNonce }}">
window.HYVOR_TALK_WEBSITE = 1234;
window.HYVOR_TALK_CONFIG = {
url: window.location.href,
id: window.location.pathname
};
</script>
<script
async
nonce="{{ .CSPNonce }}"
src="https://talk.hyvor.com/web-api/embed">
</script>
A couple of notes:
- I prefer assigning to
window.*explicitly. It’s clearer when debugging. - I still put the nonce on the external script tag. With nonce-based setups and
'strict-dynamic', that keeps trust anchored in markup you control. - If your template system exposes a request nonce, use that. Don’t hardcode one.
For Hugo specifically, many teams inject the nonce at the reverse proxy or app layer rather than generating it in Hugo itself. Static site generators don’t magically solve per-request nonce generation.
After: CSP header updated for Hyvor Talk
Here’s a practical revision of the original policy, keeping the same structure and adding Hyvor Talk support.
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://talk.hyvor.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com https://talk.hyvor.com wss://talk.hyvor.com;
frame-src 'self' https://consentcdn.cookiebot.com https://talk.hyvor.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That’s the minimal shape I’d start with.
Why these changes
script-src
Hyvor Talk’s embed script comes from https://talk.hyvor.com, so that origin needs to be permitted.
If your browser fully applies 'strict-dynamic', host allowlists may be ignored in favor of nonce/hash trust propagation. But I still keep explicit hosts during rollout because CSP behavior across browsers and third-party loaders can get messy fast.
connect-src
Comments widgets rarely stop at “load one JS file”. They fetch threads, post comments, update reactions, and sometimes maintain live connections. If Hyvor Talk uses websocket or API endpoints on the same origin, you need them here.
I usually add both:
https://talk.hyvor.com
wss://talk.hyvor.com
and tighten later if logs show a narrower set of endpoints.
frame-src
Many third-party embeds use iframes somewhere in the stack even when the top-level integration looks script-based. If you see frame violations, this is the place to allow them.
What I would not change yet
I would not immediately widen these directives unless violations prove it’s necessary:
style-srcfont-srcmedia-srcworker-src
Hyvor Talk may not need them in your setup. Don’t cargo-cult CSP entries.
A lot of CSP examples online are just giant lists copied from somebody else’s production header. That’s how policies turn into junk drawers.
If you want reference patterns for embed-friendly policies, CSP Examples is a handy starting point, but I’d still validate every directive against actual browser reports.
Safer rollout with Report-Only
When I add a new third-party widget, I usually test with Content-Security-Policy-Report-Only first.
Example:
Content-Security-Policy-Report-Only:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://talk.hyvor.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com https://talk.hyvor.com wss://talk.hyvor.com;
frame-src 'self' https://consentcdn.cookiebot.com https://talk.hyvor.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That gives you a clean way to watch what the widget tries to do before enforcing the new policy.
A cleaner alternative to inline config
If you want to avoid an inline config block completely, move configuration into data attributes or a local bootstrap file.
Example:
<div
id="hyvor-talk-view"
data-website-id="1234"
data-page-id="{{ .RelPermalink }}"
data-page-url="{{ .Permalink }}">
</div>
<script nonce="{{ .CSPNonce }}" src="/js/hyvor-bootstrap.js"></script>
Then in /js/hyvor-bootstrap.js:
const root = document.getElementById('hyvor-talk-view');
window.HYVOR_TALK_WEBSITE = Number(root.dataset.websiteId);
window.HYVOR_TALK_CONFIG = {
id: root.dataset.pageId,
url: root.dataset.pageUrl
};
const script = document.createElement('script');
script.src = 'https://talk.hyvor.com/web-api/embed';
script.async = true;
document.head.appendChild(script);
This can fit nicely with a nonce + 'strict-dynamic' setup, because the trusted bootstrap script loads the third-party script programmatically.
I like this pattern when I’m trying to reduce inline code and keep templates cleaner.
Final before/after snapshot
Before
script-src 'self' 'nonce-...' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
Result: Hyvor Talk is blocked.
After
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://talk.hyvor.com;
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com https://talk.hyvor.com wss://talk.hyvor.com;
frame-src 'self' https://consentcdn.cookiebot.com https://talk.hyvor.com;
And the embed config script gets the same nonce as the rest of your trusted inline scripts.
That’s the whole game: keep the policy shape, add the exact origins you need, and don’t throw away nonce-based protections just because a comments widget wants to run code.
If you want the browser-level details for directive behavior, the official CSP docs on MDN are still the best place to double-check edge cases around script-src and the overall Content-Security-Policy header.