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:

  1. The external Maps API script is blocked because maps.googleapis.com is not in script-src.
  2. The inline callback script is blocked unless it has a valid nonce.
  3. Map tiles or marker images may fail if img-src is too strict.
  4. Requests made by Maps can be blocked by connect-src.
  5. 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.com in script-src allows the Maps API loader.
  • connect-src usually 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.com matters if you use embeds or place details flows that create frames.
  • Google Maps often works without opening font-src or style-src much 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 style attribute from the map container. If your style-src policy 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:

  1. Ship the new policy as Content-Security-Policy-Report-Only first.
  2. Load every map feature: markers, directions, autocomplete, embeds, whatever the page uses.
  3. Watch the console and violation reports.
  4. 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.