Google Maps JavaScript API is one of those integrations that looks simple until CSP gets involved. Then you add one <script> tag, refresh, and your console turns into a wall of violations.

The hard part is that Google Maps does not just load one script. It pulls in additional scripts, images, styles, XHR/fetch requests, and sometimes fonts from multiple Google domains. If your policy is strict — and it should be — you need to allow the right sources without opening the door too wide.

Here’s the practical reference I wish I had the first few times I locked this down.

The short answer

For Google Maps JavaScript API, you will usually need to account for:

  • script-src
  • img-src
  • style-src
  • connect-src
  • sometimes font-src

Common Google Maps domains that show up:

  • https://maps.googleapis.com
  • https://*.googleapis.com
  • https://maps.gstatic.com
  • https://*.gstatic.com

If you want a copy-paste starting point, use this.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://maps.googleapis.com https://maps.gstatic.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https://maps.gstatic.com https://maps.googleapis.com;
  connect-src 'self' https://maps.googleapis.com https://*.googleapis.com https://maps.gstatic.com;
  font-src 'self' https://fonts.gstatic.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';

That is a decent baseline, but I would not call it ideal. If you already run a nonce-based strict CSP, you should keep that model and add Maps support carefully.

A realistic CSP with Google Maps added

You gave a real CSP header from headertest.com. Here it is, adapted to support Google Maps JavaScript API while preserving the existing structure.

Content-Security-Policy:
  default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
  script-src 'self' 'nonce-OWFjZjE1MWItZWY3Mi00YzU5LThjZjktMmU4NTgxZTUzMzRl' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://maps.googleapis.com https://maps.gstatic.com;
  style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com https://maps.gstatic.com https://fonts.googleapis.com;
  img-src 'self' data: https: https://maps.gstatic.com https://maps.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  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 https://maps.googleapis.com https://*.googleapis.com https://maps.gstatic.com;
  frame-src 'self' https://consentcdn.cookiebot.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  object-src 'none';

A couple of opinions here:

  • I’d keep object-src 'none', base-uri 'self', and frame-ancestors 'none' exactly as they are.
  • I would only keep 'unsafe-inline' in style-src if I had verified it was actually needed. Maps often pushes people toward it, but don’t assume.
  • If your app already uses nonces and 'strict-dynamic', don’t throw that away just because Maps is annoying.

If you want deeper CSP directive definitions, https://csp-guide.com is a good reference.

Basic script include

Most teams start with the standard Google Maps loader.

<script
  async
  src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap">
</script>

And then:

<script nonce="{{ .CSPNonce }}">
  function initMap() {
    const map = new google.maps.Map(document.getElementById("map"), {
      center: { lat: 40.7128, lng: -74.0060 },
      zoom: 10
    });
  }
</script>

If you’re using a nonce-based CSP, the inline callback script needs the nonce.

Example policy:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://maps.googleapis.com https://maps.gstatic.com;
  style-src 'self' 'unsafe-inline' https://maps.gstatic.com https://fonts.googleapis.com;
  img-src 'self' data: https://maps.gstatic.com https://maps.googleapis.com;
  connect-src 'self' https://maps.googleapis.com https://*.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  object-src 'none';
  base-uri 'self';

Better: avoid inline callback glue when possible

I generally prefer not to rely on global callback functions if I can avoid it.

<div id="map" style="height: 400px;"></div>
<script nonce="{{ .CSPNonce }}" src="/js/maps-init.js"></script>
<script
  async
  src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&loading=async&callback=initMap">
</script>

/js/maps-init.js:

window.initMap = function () {
  const mapEl = document.getElementById("map");

  new google.maps.Map(mapEl, {
    center: { lat: 51.5072, lng: -0.1276 },
    zoom: 12,
    mapTypeControl: false,
    streetViewControl: false
  });
};

That still uses a global callback because the API expects it, but at least your logic lives in a normal script file.

Strict CSP setup

If you’re serious about CSP, this is closer to what I’d deploy.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://maps.googleapis.com;
  style-src 'self' 'nonce-{{RANDOM_NONCE}}' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https://maps.gstatic.com https://maps.googleapis.com;
  connect-src 'self' https://maps.googleapis.com https://*.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';

Why only https://maps.googleapis.com in script-src here? Because with a nonce plus 'strict-dynamic', the trusted bootstrap script can load its dependencies. That’s the whole point of strict CSP. If you’re not familiar with that model, Google’s official CSP docs and https://csp-guide.com are worth reading.

That said, browsers vary, legacy behavior exists, and production CSP is never as clean as a blog post. If you see blocked script loads from maps.gstatic.com, allow it explicitly.

Common violations and fixes

1. Refused to load the script from maps.googleapis.com

Your script-src is missing Google Maps.

Fix:

script-src 'self' https://maps.googleapis.com;

If you use nonces:

script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://maps.googleapis.com;

2. Refused to connect to maps.googleapis.com or another googleapis.com endpoint

Maps is making network requests your connect-src does not allow.

Fix:

connect-src 'self' https://maps.googleapis.com https://*.googleapis.com;

3. Refused to load image from maps.gstatic.com

Map tiles, marker assets, or UI images are blocked.

Fix:

img-src 'self' data: https://maps.gstatic.com https://maps.googleapis.com;

4. Refused to apply inline style

This one is common. Google Maps often injects or relies on inline styles.

You may need:

style-src 'self' 'unsafe-inline';

I don’t love that, but sometimes it’s the realistic answer. Test whether a nonce-based style-src works in your setup before defaulting to 'unsafe-inline'.

5. Refused to load font from fonts.gstatic.com

If your map UI or surrounding page uses Google fonts, allow them explicitly.

Fix:

style-src 'self' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;

Report-Only first. Always.

Don’t ship CSP changes for Maps straight to enforcement unless you enjoy breaking production on a Friday.

Use Content-Security-Policy-Report-Only first:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://maps.googleapis.com https://maps.gstatic.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https://maps.gstatic.com https://maps.googleapis.com;
  connect-src 'self' https://maps.googleapis.com https://*.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  object-src 'none';
  base-uri 'self';

Watch the console. Watch your CSP reports. Then tighten.

Minimal HTML example

Here’s a full page you can paste into a test app.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Google Maps CSP Test</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style nonce="{{CSP_NONCE}}">
    #map {
      width: 100%;
      height: 400px;
    }
  </style>
</head>
<body>
  <h1>Google Maps CSP Test</h1>
  <div id="map"></div>

  <script nonce="{{CSP_NONCE}}">
    window.initMap = function () {
      new google.maps.Map(document.getElementById("map"), {
        center: { lat: 48.8566, lng: 2.3522 },
        zoom: 11
      });
    };
  </script>

  <script
    async
    src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap">
  </script>
</body>
</html>

Matching CSP:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{CSP_NONCE}}' 'strict-dynamic' https://maps.googleapis.com;
  style-src 'self' 'nonce-{{CSP_NONCE}}' 'unsafe-inline';
  img-src 'self' data: https://maps.gstatic.com https://maps.googleapis.com;
  connect-src 'self' https://maps.googleapis.com https://*.googleapis.com;
  object-src 'none';
  base-uri 'self';

If I were adding Google Maps JavaScript API to a modern app today, I’d start here and adjust based on actual violations:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://maps.googleapis.com https://maps.gstatic.com;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://maps.gstatic.com;
  img-src 'self' data: https://maps.gstatic.com https://maps.googleapis.com;
  connect-src 'self' https://maps.googleapis.com https://*.googleapis.com https://maps.gstatic.com;
  font-src 'self' https://fonts.gstatic.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  form-action 'self';

Then I’d remove anything I could prove was unnecessary.

That’s the real trick with CSP and Google Maps: start narrow, test hard, and only widen the policy for actual blocked behavior. Not guesses. Not cargo cult snippets copied from random issue threads.