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 default
  • script-src allows your own scripts plus nonce-based inline bootstraps
  • 'strict-dynamic' is great when your trusted nonce’d script loads other scripts
  • connect-src allows same-origin API calls and WebSockets
  • worker-src 'self' blob: matters if your editor or runtime uses workers
  • frame-ancestors 'none' blocks clickjacking
  • object-src 'none' should be standard everywhere now

If you want the directive-by-directive details, https://csp-guide.com is a good reference.

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-src for 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:

  1. I’d rather use a nonce than 'unsafe-inline' every time.
  2. 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';
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' in script-src
  • no 'unsafe-eval' unless forced
  • per-request nonces for inline bootstrap code
  • connect-src includes API and WebSocket endpoints
  • worker-src covers editor workers
  • frame-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.