Leaflet looks simple until you lock down your site with Content Security Policy. Then the map goes blank, markers disappear, plugin styles break, and you end up staring at DevTools wondering why a harmless map library suddenly needs half the internet.
I’ve hit this enough times that I treat Leaflet as a CSP integration task, not just a UI widget. The good news: most failures come from a handful of repeat mistakes.
Mistake #1: Allowing Leaflet itself but forgetting the tile server
This is the one that gets everybody first.
You load Leaflet from your own origin or a trusted CDN, the JavaScript runs fine, the map container renders, and then no tiles load. You just get a gray box.
That happens because Leaflet doesn’t ship the actual map images. Those come from a tile provider like OpenStreetMap, MapTiler, Mapbox, or your own tile endpoint. CSP sees those as images, so img-src has to allow them.
A lot of teams start from a policy like this real-world example from headertest.com:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-NzU0Zjk2YjQtZjQ1Yi00NzZjLThiY2UtMjBhNzgzODVjYjRi' '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 img-src 'self' data: https: will usually make Leaflet tiles work because it broadly allows HTTPS images. But I don’t love broad https: if you know exactly which tile host you use.
Better fix
Be explicit:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data: https://tile.openstreetmap.org;
object-src 'none';
base-uri 'self';
If you use multiple subdomains for tiles, include them:
img-src 'self' data: https://a.tile.openstreetmap.org https://b.tile.openstreetmap.org https://c.tile.openstreetmap.org;
If you want a refresher on how fetch directives fall back to default-src, the docs at https://csp-guide.com/default-src/ are worth keeping handy.
Mistake #2: Forgetting marker icons are also images
Leaflet’s default marker icons are separate image files. So even if your tiles load, your markers can silently fail if the icon URLs are blocked.
Typical symptom: map renders, tiles render, but markers are invisible.
Leaflet’s defaults usually reference files like:
marker-icon.pngmarker-icon-2x.pngmarker-shadow.png
If those files are served from your own app, img-src 'self' covers them. If your bundler rewrites paths in weird ways, or you serve assets from a CDN, you need to allow that source too.
Broken setup
L.marker([51.5, -0.09]).addTo(map);
With CSP:
img-src 'self' https://tile.openstreetmap.org;
If marker assets come from https://cdn.example.com, they’ll be blocked.
Fix
img-src 'self' data: https://tile.openstreetmap.org https://cdn.example.com;
Or avoid surprise network requests and set the icon URLs yourself:
import L from "leaflet";
import marker2x from "leaflet/dist/images/marker-icon-2x.png";
import marker from "leaflet/dist/images/marker-icon.png";
import shadow from "leaflet/dist/images/marker-shadow.png";
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: marker2x,
iconUrl: marker,
shadowUrl: shadow,
});
I prefer this in production anyway. It makes asset paths predictable, which makes CSP easier.
Mistake #3: Blocking Leaflet CSS or plugin CSS
Leaflet without CSS is a mess. Controls stack wrong, markers misalign, popups look broken, and touch interactions get weird.
People often remember script-src and img-src, then forget style-src.
Broken policy
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
That only works if:
leaflet.cssis hosted on your own origin- you don’t use inline styles
- your plugins don’t inject styles
The headertest.com policy uses:
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
That’s common in the real world, but if you’re tightening CSP, unsafe-inline is usually the part you want to get rid of.
Fix
Serve Leaflet CSS from your own origin if possible:
style-src 'self';
And include it normally:
<link rel="stylesheet" href="/assets/leaflet/leaflet.css">
If you absolutely must use a CDN-hosted stylesheet, add that exact origin to style-src.
Mistake #4: Using inline map setup without a nonce or hash
A lot of Leaflet examples look like this:
<div id="map"></div>
<script>
const map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
</script>
That’s fine until your CSP blocks inline scripts.
If your policy is nonce-based, like the headertest.com example:
script-src 'self' 'nonce-NzU0Zjk2YjQtZjQ1Yi00NzZjLThiY2UtMjBhNzgzODVjYjRi' 'strict-dynamic';
then your inline Leaflet setup needs the matching nonce.
Fix with nonce
<div id="map"></div>
<script nonce="{{ .CSPNonce }}">
const map = L.map('map').setView([51.505, -0.09], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);
</script>
Better yet, move it into an external file:
<script src="/js/map.js"></script>
That’s cleaner and usually easier to maintain than chasing inline script hashes.
For nonce and hash details, https://csp-guide.com/script-src/ is the page I’d point people to.
Mistake #5: Assuming all map data comes through img-src
Not always.
Classic raster tiles use img-src. But some Leaflet setups fetch GeoJSON, vector tiles, routing data, geocoding responses, or custom overlays over XHR/fetch. Those need connect-src.
Typical example:
fetch("/api/places")
.then((res) => res.json())
.then((places) => {
L.geoJSON(places).addTo(map);
});
If that API is cross-origin, connect-src has to allow it.
Fix
connect-src 'self' https://api.example.com;
If you use WebSockets for live location updates:
connect-src 'self' https://api.example.com wss://ws.example.com;
I’ve seen teams waste time tweaking img-src for GeoJSON bugs. Wrong directive. If it’s fetch, XHR, EventSource, or WebSocket, check connect-src.
Mistake #6: Allowing https: for images because it “just works”
This one is less a bug and more a lazy shortcut.
Yes, this works:
img-src 'self' data: https:;
That’s what the headertest.com policy does, and for a general-purpose app it may be a practical tradeoff. But for Leaflet maps, it usually means you’re allowing every HTTPS image on the internet just to load tiles and marker assets.
That’s wider than most apps need.
Better fix
List the exact hosts:
img-src 'self' data:
https://tile.openstreetmap.org
https://cdn.example.com;
If your map provider rotates across subdomains, allow those specific subdomains or a scoped wildcard when necessary:
img-src 'self' data: https://*.tiles.example.com;
Scoped wildcard beats https: every time.
Mistake #7: Third-party Leaflet plugins sneaking in extra CSP requirements
Leaflet plugins are where clean CSP setups go to die.
A clustering plugin may be fine. A geocoder plugin may call a remote API. A fullscreen plugin may be harmless. A drawing plugin may inject inline styles. A heatmap plugin may use data URLs or canvas exports.
Don’t assume “it’s a Leaflet plugin” means the same CSP rules apply.
Fix
Audit each plugin by resource type:
- JavaScript file:
script-src - CSS file:
style-src - Tile or icon images:
img-src - API requests:
connect-src - Fonts:
font-src - Worker usage: maybe
worker-src
My rule: add one plugin at a time with CSP report-only enabled first, then tighten after you see what it actually needs.
Mistake #8: Forgetting data: for generated images
Some Leaflet extensions or custom marker strategies use base64/data URL images. If your CSP blocks data: in img-src, those markers or overlays may disappear.
Fix
img-src 'self' data: https://tile.openstreetmap.org;
I don’t add data: automatically everywhere, but for image-heavy map UIs it’s often necessary.
A practical CSP baseline for Leaflet
If I were starting from scratch with self-hosted Leaflet assets, OpenStreetMap tiles, local marker images, and an API for GeoJSON, I’d use something like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self';
img-src 'self' data: https://tile.openstreetmap.org;
connect-src 'self' https://api.example.com;
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
And if you still have some unavoidable inline styles from legacy UI code, I’d isolate and remove those instead of immediately reaching for style-src 'unsafe-inline'.
What I check first when a Leaflet map breaks under CSP
My debugging order is always the same:
- Blank map tiles →
img-src - Missing markers →
img-srcfor marker assets ordata: - Broken layout/controls →
style-src - Map init script blocked →
script-srcnonce/hash - GeoJSON or search not loading →
connect-src - Only plugin features failing → inspect that plugin’s network and DOM behavior
Leaflet itself is not especially hostile to CSP. The pain usually comes from asset paths, tile hosts, inline setup scripts, and plugins that quietly pull in more dependencies than you expected.
If you treat the map as a bundle of resource types instead of “one library,” CSP gets a lot easier.