Google Maps is one of those integrations that looks trivial right up until CSP starts blocking half of it.
I’ve seen this play out a few times: the site already has a decent policy, someone drops in a Maps embed or the JavaScript API, and suddenly the console fills with CSP errors. The quick fix is usually script-src https://maps.googleapis.com 'unsafe-inline' plus a couple of random domains copied from Stack Overflow. That works, but it also turns a decent policy into a mushy one.
Here’s a cleaner way to do it, based on a real-world style of policy and the kind of problems teams hit in production.
The starting point
A lot of sites already have a policy similar to the one reported for headertest.com:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-ODc2MjcwYzgtOWJkMy00MDViLWExNDgtMjkxNzU1OTQwOTBi' '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://app.tallytics.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 bad baseline. It has a nonce, uses strict-dynamic, and avoids obvious foot-guns like object-src *. For analytics and consent tooling, it’s reasonably tight.
Then the product team asks for a store locator page with Google Maps.
Before: the integration that breaks under CSP
Here’s the kind of code people add first.
<div id="map" style="height: 400px;"></div>
<script
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
async
defer></script>
<script>
function initMap() {
const center = { lat: 40.7128, lng: -74.0060 };
const map = new google.maps.Map(document.getElementById("map"), {
zoom: 12,
center
});
new google.maps.Marker({
position: center,
map
});
}
</script>
Looks normal. Under the CSP above, it usually fails in a few different ways:
- The external Maps API script is blocked because
maps.googleapis.comis not inscript-src. - The inline callback script is blocked unless it has a valid nonce.
- Map tiles or marker images may fail if
img-srcis too strict. - Requests made by Maps can be blocked by
connect-src. - Depending on the integration, styles or fonts may also need explicit allowances.
Typical console noise looks like this:
Refused to load the script 'https://maps.googleapis.com/maps/api/js?...'
because it violates the following Content Security Policy directive:
"script-src 'self' 'nonce-...' 'strict-dynamic' https://www.googletagmanager.com ..."
And then:
Refused to execute inline script because it violates the following Content Security Policy directive:
"script-src 'self' 'nonce-...' 'strict-dynamic' ..."
This is where teams usually panic and start loosening the policy globally.
The wrong fix
I’ve inherited policies like this after a rushed release:
Content-Security-Policy:
default-src * data: blob: 'unsafe-inline' 'unsafe-eval';
script-src * data: blob: 'unsafe-inline' 'unsafe-eval';
style-src * 'unsafe-inline';
img-src * data: blob:;
connect-src *;
frame-src *;
Yes, the map works. So does any injected script an attacker manages to land on the page.
That’s not a CSP anymore. That’s a checkbox.
After: a production-ready fix
The better approach is to update the policy based on how Google Maps actually loads resources, while keeping the existing nonce-based model intact.
For a JavaScript API integration, I’d start here:
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://maps.googleapis.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com https://fonts.googleapis.com;
img-src 'self' data: https:;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.headertest.com https://app.tallytics.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com https://maps.googleapis.com https://*.googleapis.com https://*.gstatic.com;
frame-src 'self' https://consentcdn.cookiebot.com https://www.google.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
A few notes:
https://maps.googleapis.cominscript-srcallows the Maps API loader.connect-srcusually needs Google API and static asset endpoints. I prefer adding only what the app actually uses, then tightening after observing reports.img-src https:already covers map tiles and marker assets in many setups. If your policy was stricter, this is where maps often break.frame-src https://www.google.commatters if you use embeds or place details flows that create frames.- Google Maps often works without opening
font-srcorstyle-srcmuch further than this, but it depends on your exact UI and whether you load Google Fonts alongside the map.
If you want a refresher on directive behavior, the official CSP docs are worth keeping open while testing: MDN Content Security Policy. For directive-by-directive examples, csp-guide.com is handy.
Fixing the page code too
The header alone isn’t enough. The page needs to stop relying on non-nonced inline code.
Here’s the cleaned-up version.
<div id="map" class="map"></div>
<script nonce="{{ .CSPNonce }}">
window.initStoreMap = function () {
const center = { lat: 40.7128, lng: -74.0060 };
const map = new google.maps.Map(document.getElementById("map"), {
zoom: 12,
center,
mapTypeControl: false,
streetViewControl: false
});
new google.maps.Marker({
position: center,
map,
title: "Main Office"
});
};
</script>
<script
nonce="{{ .CSPNonce }}"
src="https://maps.googleapis.com/maps/api/js?key={{ .MapsAPIKey }}&callback=initStoreMap"
async
defer></script>
And the CSS:
.map {
height: 400px;
width: 100%;
}
A couple of practical choices here:
- I removed the inline
styleattribute from the map container. If yourstyle-srcpolicy is already carrying'unsafe-inline', you can get away with it, but I’d rather not add more inline styling than necessary. - The callback function is defined inside a nonced script block, so it works with the existing nonce model.
- I keep the API key server-rendered and locked down by referrer restrictions on the Google side. CSP is not your API key protection mechanism.
What changed, exactly?
Here’s the before-and-after view that usually helps teams.
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://app.tallytics.com ... https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
After
script-src 'self' 'nonce-...' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://maps.googleapis.com;
connect-src 'self' https://api.headertest.com https://app.tallytics.com ... https://*.cookiebot.com https://maps.googleapis.com https://*.googleapis.com https://*.gstatic.com;
frame-src 'self' https://consentcdn.cookiebot.com https://www.google.com;
That’s the whole game: add the minimum Google Maps sources needed, keep the nonce flow, and don’t weaken the rest of the policy.
One thing that trips people up with strict-dynamic
If you’re using a nonce plus strict-dynamic, modern browsers will trust scripts loaded by your trusted nonced script. That’s great, but it also means your mental model should be “which root scripts do I trust?” not “did I list every possible child script host?”
I still include https://maps.googleapis.com explicitly because it helps with compatibility and keeps the policy readable for humans. But the nonce is doing the heavy lifting.
If your app has old browser support requirements, test more aggressively. CSP behavior around strict-dynamic and fallback host lists isn’t where you want to make assumptions.
Debugging this in production without guessing
My usual process:
- Ship the new policy as
Content-Security-Policy-Report-Onlyfirst. - Load every map feature: markers, directions, autocomplete, embeds, whatever the page uses.
- Watch the console and violation reports.
- Add only the sources that correspond to real blocked requests.
For report-only testing:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://maps.googleapis.com;
connect-src 'self' https://maps.googleapis.com https://*.googleapis.com https://*.gstatic.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
report-to csp-endpoint;
If you already have a mature baseline policy, don’t replace it with a tiny test policy like this in production. Layer the changes onto your real one.
My rule for third-party widgets
Every third-party integration wants to become the center of your CSP. Don’t let it.
Google Maps is useful, but it doesn’t get to justify:
unsafe-eval- global wildcards
- broad
frame-src * - turning your nonce model into “just allow inline”
If Maps needs a few well-scoped additions, fine. If a snippet tells you to open half the web, treat that snippet as untrusted until proven otherwise.
That’s the real before-and-after story here. The “before” version is a good site policy that breaks because it’s incomplete. The “after” version is still a good site policy, just with enough room for Google Maps to function.
That’s what you want from CSP: not perfection, just deliberate trust boundaries.