If you’re building a Go playground-style app, CSP gets weird fast.
A regular Go web app might serve a few static JS files and call it a day. A playground usually does more:
- runs user code or sends it to a backend sandbox
- opens WebSocket connections for logs or interactive output
- injects bootstrapping data into the page
- embeds editors like Monaco or CodeMirror
- uses inline scripts because templating makes it convenient
That combination is exactly where sloppy CSP setups happen.
This guide is a practical reference for locking down a Go playground without breaking the page every five minutes.
A solid baseline CSP for a Go playground
Start here if your playground is mostly self-hosted and doesn’t rely on third-party scripts.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{.CSPNonce}}' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' wss:;
worker-src 'self' blob:;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
What this does:
default-src 'self'locks everything to your own origin by defaultscript-srcallows your own scripts plus nonce-based inline bootstraps'strict-dynamic'is great when your trusted nonce’d script loads other scriptsconnect-srcallows same-origin API calls and WebSocketsworker-src 'self' blob:matters if your editor or runtime uses workersframe-ancestors 'none'blocks clickjackingobject-src 'none'should be standard everywhere now
If you want the directive-by-directive details, https://csp-guide.com is a good reference.
Real-world example: production CSP with analytics and consent tools
Here’s a real CSP header captured from headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-ZWRlNTUxZjQtMTQzOC00OTJlLWI2MDYtN2U5ZGQ2ZjY2YjE5' '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'
A few things I like here:
- nonce-based scripts instead of
'unsafe-inline' frame-ancestors 'none'object-src 'none'- explicit
connect-srcfor APIs and WebSockets
A few things I’d question for a playground app:
style-src 'unsafe-inline'is often a compromise, not a win- broad third-party allowances pile up quickly
img-src https:is convenient, but wider than most apps need
For a Go playground, I’d keep the shape of this policy but try hard to self-host assets and avoid third-party script dependencies.
Go middleware to set CSP headers
Here’s a copy-paste middleware that generates a per-request nonce and sets a CSP header.
package main
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
)
type contextKey string
const cspNonceKey contextKey = "cspNonce"
func generateNonce() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(b), nil
}
func CSPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nonce, err := generateNonce()
if err != nil {
http.Error(w, "failed to generate CSP nonce", http.StatusInternalServerError)
return
}
csp := fmt.Sprintf(
"default-src 'self'; "+
"script-src 'self' 'nonce-%s' 'strict-dynamic'; "+
"style-src 'self'; "+
"img-src 'self' data:; "+
"font-src 'self'; "+
"connect-src 'self' wss:; "+
"worker-src 'self' blob:; "+
"base-uri 'self'; "+
"form-action 'self'; "+
"frame-ancestors 'none'; "+
"object-src 'none'",
nonce,
)
w.Header().Set("Content-Security-Policy", csp)
ctx := context.WithValue(r.Context(), cspNonceKey, nonce)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func CSPNonce(r *http.Request) string {
if v, ok := r.Context().Value(cspNonceKey).(string); ok {
return v
}
return ""
}
Use it like this:
mux := http.NewServeMux()
mux.Handle("/", CSPMiddleware(http.HandlerFunc(playgroundHandler)))
http.ListenAndServe(":8080", mux)
Using the nonce in Go templates
Most playground UIs need a tiny inline bootstrap script. That’s fine. Just nonce it.
package main
import (
"html/template"
"net/http"
)
var pageTmpl = template.Must(template.New("page").Parse(`
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Go Playground</title>
<link rel="stylesheet" href="/static/app.css">
</head>
<body>
<div id="app"></div>
<script nonce="{{.Nonce}}">
window.PLAYGROUND_CONFIG = {
runURL: "/api/run",
wsURL: "wss://" + location.host + "/ws"
};
</script>
<script nonce="{{.Nonce}}" src="/static/app.js"></script>
</body>
</html>
`))
func playgroundHandler(w http.ResponseWriter, r *http.Request) {
data := struct {
Nonce string
}{
Nonce: CSPNonce(r),
}
if err := pageTmpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
Two opinions here:
- I’d rather use a nonce than
'unsafe-inline'every time. - I try to keep inline bootstraps tiny. Once they grow real logic, they belong in a file.
CSP for WebSockets and run APIs
A Go playground often needs both HTTP and WebSocket connections.
Typical setup:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{.CSPNonce}}';
style-src 'self';
img-src 'self' data:;
connect-src 'self' https://sandbox.internal.example wss://sandbox.internal.example;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
Use connect-src for:
fetch()- XHR
- EventSource
- WebSockets
sendBeacon()
If your frontend talks to /api/run on the same host and /ws on the same host, this is enough:
connect-src 'self' wss:;
If you forget wss:, your socket connection will fail in a way that looks like an app bug instead of a policy bug. I’ve lost time to that one.
If your editor uses workers
Monaco and similar browser editors often use Web Workers and sometimes blob: URLs.
You’ll usually need:
worker-src 'self' blob:;
script-src 'self' 'nonce-{{.CSPNonce}}';
If you see CSP errors mentioning worker-src or blocked blob: execution, this is probably the missing piece.
For some builds, you may also need to change how the editor assets are bundled so workers are served as normal files instead of generated blobs. That’s cleaner from a CSP perspective.
Avoid 'unsafe-eval' unless you absolutely have to
Some playground UIs or editor toolchains try to sneak in eval() or new Function().
That pushes people toward this:
script-src 'self' 'unsafe-eval';
I avoid that unless there’s no realistic alternative. For a playground, users already execute code in one part of the system. You don’t want to also weaken browser-side script policy for convenience.
If a library requires 'unsafe-eval', I’d first ask:
- can I use a production build instead of a dev build?
- can I switch bundlers?
- can I replace the library?
That sounds harsh, but 'unsafe-eval' is usually a smell.
Report-only mode for debugging
When tightening CSP on a playground, use report-only first.
w.Header().Set("Content-Security-Policy-Report-Only",
"default-src 'self'; script-src 'self' 'nonce-"+nonce+"'; connect-src 'self' wss:; object-src 'none';")
This lets the browser report violations without blocking them.
The Go standard library docs for headers and handlers are here:
And the official CSP docs from MDN are still the fastest reference when a browser error is vague:
Common CSP recipes for Go playgrounds
1. Minimal self-hosted playground
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{.CSPNonce}}';
style-src 'self';
img-src 'self' data:;
connect-src 'self' wss:;
worker-src 'self' blob:;
base-uri 'self';
frame-ancestors 'none';
object-src 'none';
2. Playground with third-party analytics
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{.CSPNonce}}' 'strict-dynamic' https://www.googletagmanager.com https://*.google-analytics.com;
style-src 'self';
img-src 'self' data: https://*.google-analytics.com;
connect-src 'self' wss: https://*.google-analytics.com https://*.googletagmanager.com;
worker-src 'self' blob:;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
3. Playground with embedded consent banner tooling
Content-Security-Policy:
default-src 'self' https://*.cookiebot.com;
script-src 'self' 'nonce-{{.CSPNonce}}' 'strict-dynamic' https://*.cookiebot.com;
style-src 'self' 'unsafe-inline' https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
connect-src 'self' wss: https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
worker-src 'self' blob:;
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
object-src 'none';
I’d treat this as a compromise policy, not a clean baseline.
Final checklist
For a Go playground, I check these first:
- no
'unsafe-inline'inscript-src - no
'unsafe-eval'unless forced - per-request nonces for inline bootstrap code
connect-srcincludes API and WebSocket endpointsworker-srccovers editor workersframe-ancestors 'none'object-src 'none'- third-party domains kept to the absolute minimum
If your playground is small, self-host your assets and keep the CSP boring. Boring CSP is good CSP. The more vendors and inline shortcuts you add, the more time you’ll spend reading console errors instead of shipping features.