Geo-distributed checks never render a map - Content-Security-Policy blocks content
Describe the bug
Geo-distributed checks never render a map. The app's own Content-Security-Policy blocks both the MapLibre web worker and the basemap style it fetches, so the map panel stays empty and the console fills with CSP violations.
Two directives are responsible, both in the helmet config in server/src/app.ts:
script-srcdoes not includeblob:, andworker-srcis not set. MapLibre creates its worker from ablob:URL, which falls back toscript-srcand is refused.- There is no
connect-srcentry for the Carto basemap hosts, so the style, tiles, glyphs and sprite are all refused againstdefault-src 'self'.
client/src/Components/monitors/GeoChecksMap.tsx hardcodes https://basemaps.cartocdn.com/gl/{positron,dark-matter}-gl-style/style.json, which the shipped policy forbids.
The policy is only applied to documents served by the SPA catch-all. app.use(express.static(frontendPath)) is registered before helmet, so GET / is returned with no CSP header at all. Navigating to a monitor from the dashboard leaves the map working, while deep-linking or reloading the same monitor page breaks it. That inconsistency is probably why this has gone unnoticed.
To Reproduce
Steps to reproduce the behavior:
- Run
ghcr.io/bluewave-labs/checkmate:developbehind the Node server (not the Vite dev server), so helmet is in the request path. - Edit an uptime monitor and enable "Geo-Distributed Checks".
- Navigate directly to
/uptime/<monitorId>, or reload the page if you arrived by clicking through from the dashboard. - Open the console. The map area is blank.
Observed console output:
Creating a worker from 'blob:https://<host>/<uuid>' violates the following
Content Security Policy directive: "script-src 'self' 'unsafe-inline' 'unsafe-eval'".
Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.
The action has been blocked.
Connecting to 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json'
violates the following Content Security Policy directive: "connect-src 'self'".
The action has been blocked.
AJAXError: Failed to fetch (0):
https://basemaps.cartocdn.com/gl/positron-gl-style/style.jsonHeader served on /uptime/<monitorId>:
default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self';
frame-ancestors 'self'; img-src 'self' data: blob: https://img.shields.io;
object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval';
script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'Header served on /: none.
Expected behavior
The map renders and shows check results by location, on a direct page load as well as a client-side navigation.
Screenshots
The map panel renders empty - as you can see above there are just dots!
Desktop (please complete the following information):
- Browser Chrome
- Version 152.0.7977.83
Note - this issue is not specific to this browser - Safari renders identically.
Additional context
The map needs three groups of hosts, which are not all obvious from the first console error:
| Resource | Host |
|---|---|
style.json |
basemaps.cartocdn.com |
TileJSON, glyphs, sprite .json and .png |
tiles.basemaps.cartocdn.com |
Vector tiles .mvt |
tiles-a … tiles-d.basemaps.cartocdn.com |
A CSP allowing only basemaps.cartocdn.com resolves the first request and then fails on the tiles, because a CSP wildcard does not match the apex domain. Both entries are needed.
Everything except the worker lands on connect-src. Glyphs are .pbf fetched with fetch, so font-src is not involved. Sprite images also go through fetch rather than an HTMLImageElement, because MapLibre deliberately avoids HTMLImageElement for sprites, so img-src needs no change — the existing blob: entry already covers the ImageBitmap fallback path.
Suggested directives:
"worker-src": ["'self'", "blob:"],
"connect-src": ["'self'", "https://basemaps.cartocdn.com", "https://*.basemaps.cartocdn.com"],It is worth deciding separately whether express.static should sit behind helmet, so that every document carries the policy rather than only those served by the catch-all.
Verified against ghcr.io/bluewave-labs/checkmate:develop (image built 2026-09-15), maplibre-gl 5.19.0, and the live Carto style and TileJSON responses. The suggested directives are derived from those responses and from the MapLibre request code; they have not been confirmed by a browser run with the widened policy in place.
Source: bluewave-labs/Checkmate