Google Custom Search looks simple until you put a real Content Security Policy in front of it. Then things break in annoying, non-obvious ways: scripts stop loading, inline styles get blocked, result iframes fail, and your console turns into a CSP crime scene.
I’ve had this happen more than once. The usual mistake is starting with a clean locked-down policy and assuming Google’s search widget behaves like a normal self-hosted component. It doesn’t. It pulls scripts, images, styles, and frames from multiple Google domains, and if you miss even one, the widget half-renders or silently fails.
Here’s how I’d approach CSP for Google Custom Search on a production site.
What Google Custom Search needs
The embedded Programmable Search Engine widget typically relies on:
- Google-hosted JavaScript
- Google-hosted styles
- Google domains for images and search requests
- iframes for some rendering flows
That means your CSP usually needs at least:
script-srcstyle-srcimg-srcconnect-srcframe-src- sometimes
font-src
If your site already has analytics, tag managers, or consent tools, you’ll merge those requirements into the same policy.
For reference, here’s a real-world CSP header from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-ZTAyYjc0YTEtYTY2YS00MmY2LWIyNmUtMDM0OTc1YmZiMmYz' '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 policy is solid for its own stack, but it does not allow Google Custom Search yet. You’d need to add the right Google sources.
Basic Google Custom Search embed
A typical embed looks like this:
<script async src="https://cse.google.com/cse.js?cx=YOUR_ENGINE_ID"></script>
<div class="gcse-search"></div>
That one line of script is where CSP trouble starts.
At minimum, the browser must allow:
https://cse.google.cominscript-src- likely
https://cse.google.comandhttps://www.google.cominframe-src - Google image domains in
img-src - Google endpoints in
connect-src - inline styles or Google style origins depending on widget behavior
Start with Report-Only
Do this first. Don’t guess.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; report-to csp-endpoint;
Or with the older reporting directive:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; report-uri /csp-report;
Load the page, use the search box, paginate results, and inspect violations in DevTools or your reporting endpoint.
You’ll usually see blocked requests from Google domains you need to allow.
A practical CSP for Google Custom Search
Here’s a policy that is a good starting point for a site embedding Google Custom Search and nothing especially weird beyond that:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cse.google.com https://www.google.com;
style-src 'self' 'unsafe-inline' https://cse.google.com https://www.google.com;
img-src 'self' data: https://www.google.com https://www.gstatic.com https://cse.google.com;
connect-src 'self' https://www.google.com https://cse.google.com;
frame-src 'self' https://www.google.com https://cse.google.com;
font-src 'self' https://www.gstatic.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
A few blunt opinions:
- I don’t like
'unsafe-inline'instyle-src, but Google widgets sometimes force your hand. - I would not add
https:broadly unless I had no choice. - I would avoid putting Google domains into
default-src. Be explicit per directive.
If you already use nonces and strict-dynamic
A lot of modern apps already run something like this in script-src:
script-src 'self' 'nonce-rAnd0m123' 'strict-dynamic';
That works great for your own trusted bootstrapping scripts, but third-party widgets can still get awkward. If you load the Google Custom Search script from a nonce-bearing script tag, strict-dynamic may help for descendant scripts, but you still need to test the actual runtime behavior.
Example with a nonce:
<script nonce="{{ .CSPNonce }}" async src="https://cse.google.com/cse.js?cx=YOUR_ENGINE_ID"></script>
<div class="gcse-search"></div>
And the matching header:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://cse.google.com https://www.google.com;
style-src 'self' 'unsafe-inline' https://cse.google.com https://www.google.com;
img-src 'self' data: https://www.google.com https://www.gstatic.com https://cse.google.com;
connect-src 'self' https://www.google.com https://cse.google.com;
frame-src 'self' https://www.google.com https://cse.google.com;
object-src 'none';
base-uri 'self';
form-action 'self';
If your app already follows the kind of pattern seen in the headertest.com header, extend the existing directives rather than replacing them.
Extending an existing production CSP
Using the headertest.com policy as a base, a merged version for Google Custom Search might look like this:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-ZTAyYjc0YTEtYTY2YS00MmY2LWIyNmUtMDM0OTc1YmZiMmYz' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://cse.google.com https://www.google.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com https://cse.google.com https://www.google.com;
img-src 'self' data: https: https://www.gstatic.com https://cse.google.com https://www.google.com;
font-src 'self' https://www.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://cse.google.com https://www.google.com;
frame-src 'self' https://consentcdn.cookiebot.com https://cse.google.com https://www.google.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s not guaranteed to be the final version, because Google can vary behavior by widget mode and region. But it’s a realistic working baseline.
Nginx example
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' https://cse.google.com https://www.google.com;
style-src 'self' 'unsafe-inline' https://cse.google.com https://www.google.com;
img-src 'self' data: https://www.google.com https://www.gstatic.com https://cse.google.com;
connect-src 'self' https://www.google.com https://cse.google.com;
frame-src 'self' https://www.google.com https://cse.google.com;
font-src 'self' https://www.gstatic.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
" always;
Express example
If you set CSP in Node/Express, I’d strongly recommend generating it from arrays so you don’t create an unreadable string mess six months later.
app.use((req, res, next) => {
const policy = {
"default-src": ["'self'"],
"script-src": ["'self'", "https://cse.google.com", "https://www.google.com"],
"style-src": ["'self'", "'unsafe-inline'", "https://cse.google.com", "https://www.google.com"],
"img-src": ["'self'", "data:", "https://www.google.com", "https://www.gstatic.com", "https://cse.google.com"],
"connect-src": ["'self'", "https://www.google.com", "https://cse.google.com"],
"frame-src": ["'self'", "https://www.google.com", "https://cse.google.com"],
"font-src": ["'self'", "https://www.gstatic.com"],
"object-src": ["'none'"],
"base-uri": ["'self'"],
"form-action": ["'self'"],
"frame-ancestors": ["'none'"]
};
const header = Object.entries(policy)
.map(([directive, values]) => `${directive} ${values.join(" ")}`)
.join("; ");
res.setHeader("Content-Security-Policy", header);
next();
});
Common breakages
1. Search box loads, results do not
Usually frame-src or connect-src.
Check for blocked URLs like:
https://www.google.com/...https://cse.google.com/...
2. Widget appears unstyled
Usually style-src.
You may need:
- Google origins in
style-src 'unsafe-inline'for inline widget styles
I don’t love it, but I’ve seen this be the practical fix.
3. Icons or thumbnails are broken
Usually img-src or font-src.
Google often uses gstatic for static assets.
4. It works in one environment but not another
That’s often because one environment has a nonce-based script loader or a different consent manager modifying script execution order.
Third-party widgets plus tag managers plus CSP is where “works on staging” goes to die.
Debugging workflow that actually works
My process is pretty boring, but it saves time:
- Deploy with
Content-Security-Policy-Report-Only - Load the page with the search widget
- Perform real searches, not just page load
- Open DevTools Console and Network
- Add only the blocked sources you actually need
- Convert to enforcing mode
- Re-test after every Google widget config change
Don’t start with giant wildcard policies. They tend to stick around forever.
Keep the policy narrow
A lot of examples online solve this by doing some variation of:
script-src 'self' https: 'unsafe-inline' 'unsafe-eval';
img-src * data:;
frame-src *;
That’s not a CSP. That’s a decorative header.
If you want ready-to-use CSP policy patterns, you can also keep examples in your internal docs or adapt templates from https://csp-examples.com. For directive behavior and browser handling, the official references are the MDN CSP documentation and the CSP spec.
Final recommended baseline
If you want a sane default to test first for Google Custom Search, use this:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cse.google.com https://www.google.com;
style-src 'self' 'unsafe-inline' https://cse.google.com https://www.google.com;
img-src 'self' data: https://www.google.com https://www.gstatic.com https://cse.google.com;
connect-src 'self' https://www.google.com https://cse.google.com;
frame-src 'self' https://www.google.com https://cse.google.com;
font-src 'self' https://www.gstatic.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Then tighten from there based on actual violations.
That’s the real trick with CSP and Google Custom Search: don’t aim for a perfect policy on the first pass. Aim for an explicit, testable policy that allows exactly what the widget uses on your site, in your configuration, with your other third-party baggage.