If you’re building a Foundation site, CSP usually gets messy in two places fast: JavaScript plugins and inline styles. Foundation itself isn’t uniquely hard to secure, but the usual stack around it—jQuery, what-input, CDN assets, analytics, consent banners—turns a clean policy into a pile of exceptions if you’re not careful.
This guide is the version I wish I had when tightening CSP on a real Foundation app: minimal theory, lots of working policies.
A solid starting point for Foundation
For a self-hosted Foundation setup with local CSS and JS, start here:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
This is strict enough to block most garbage, while still being realistic for a normal app.
What this does
default-src 'self'locks everything to your own origin unless overridden.script-src 'self'allows only your own JS files.style-src 'self'allows only your own CSS files.img-src 'self' data:keeps local images working and allows inline data URIs, which some UI code and email-like assets still use.object-src 'none'kills Flash-style legacy plugin nonsense.base-uri 'self'stops attackers from rewriting relative URLs with a malicious<base>.frame-ancestors 'none'blocks clickjacking by preventing your site from being framed.
If you want directive-by-directive background, csp-guide.com is a good reference without being painfully academic.
Basic Foundation page that works with this CSP
Self-hosted assets are the easy path.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Foundation + CSP</title>
<link rel="stylesheet" href="/assets/foundation.min.css">
<link rel="stylesheet" href="/assets/app.css">
</head>
<body>
<div class="grid-container">
<h1>Hello Foundation</h1>
<button class="button" data-open="exampleModal1">Open modal</button>
</div>
<div class="reveal" id="exampleModal1" data-reveal>
<p>This modal works fine under CSP.</p>
<button class="close-button" data-close>×</button>
</div>
<script src="/assets/jquery.min.js"></script>
<script src="/assets/what-input.min.js"></script>
<script src="/assets/foundation.min.js"></script>
<script src="/assets/app.js"></script>
</body>
</html>
And initialize Foundation in your app JS file, not inline:
$(document).foundation();
That matters because the first thing people break with CSP is dropping this into an inline <script> block.
Avoid inline scripts if you can
This will be blocked:
<script>
$(document).foundation();
</script>
If you want a strict policy, move it into /assets/app.js:
document.addEventListener('DOMContentLoaded', function () {
$(document).foundation();
});
Then keep:
script-src 'self';
That’s the cleanest setup.
If you must use inline scripts, use a nonce
Sometimes you have server-rendered templates and one tiny inline bootstrap script. Fine. Use a nonce, not 'unsafe-inline'.
CSP header
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnd0mBase64Value';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
HTML
<script nonce="rAnd0mBase64Value">
$(document).foundation();
</script>
Your app must generate a fresh nonce per response. Reusing a static nonce defeats the point.
Express example
import crypto from 'node:crypto';
import express from 'express';
const app = express();
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self'",
"img-src 'self' data:",
"font-src 'self'",
"connect-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'"
].join('; ')
);
next();
});
Then in your template:
<script nonce="{{nonce}}">
$(document).foundation();
</script>
Inline styles are where Foundation-adjacent projects go sideways
Foundation CSS itself is fine when loaded from a file. The problem is your app, plugins, or third-party widgets injecting inline styles.
The lazy fix is this:
style-src 'self' 'unsafe-inline';
That works, but I don’t love it. It weakens CSP a lot.
Still, if you’re integrating a consent tool, tag manager snippets, or legacy Foundation templates, you may need it temporarily.
A practical transitional policy looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
My advice: allow 'unsafe-inline' for styles only if you’ve confirmed what actually needs it, then work toward removing it.
Using Foundation from a CDN
If you load Foundation, jQuery, or what-input from a CDN, add those origins explicitly.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net https://code.jquery.com;
style-src 'self' https://cdn.jsdelivr.net;
img-src 'self' data:;
font-src 'self' https://cdn.jsdelivr.net;
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Example page:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/foundation.min.css">
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/what-input.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/foundation.min.js"></script>
<script src="/assets/app.js"></script>
Self-hosting is still better. Fewer CSP exceptions, fewer supply-chain dependencies, fewer surprises.
Foundation + analytics + consent banners
This is where real policies stop looking pretty.
Here’s a real-world CSP from headertest.com:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-NjY4MmY3OTEtM2M2Zi00MjZjLTk4NGEtYzlmYTljNGMzMDk4' '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’s a good example of how production CSPs actually look: not minimal, but intentional.
A Foundation site with Google Tag Manager and Google Analytics might use something like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RUNTIME_NONCE' 'strict-dynamic'
https://www.googletagmanager.com
https://www.google-analytics.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https://www.google-analytics.com;
font-src 'self';
connect-src 'self'
https://www.google-analytics.com
https://region1.google-analytics.com
https://www.googletagmanager.com;
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
A couple of opinions here:
strict-dynamicis excellent if you’re using nonces correctly.- GTM tends to expand your trust boundary. That’s not automatically bad, but you should admit that’s what you’re doing.
- Consent tools often force
style-src 'unsafe-inline'and extraframe-src/script-srcentries.
Report-only mode before enforcement
Don’t roll out a new CSP blind on a Foundation site with plugins and marketing tags. Use report-only first.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
report-uri /csp-report;
Example reporting endpoint in Express:
app.post('/csp-report', express.json({ type: ['application/csp-report', 'application/json'] }), (req, res) => {
console.log('CSP violation:', JSON.stringify(req.body, null, 2));
res.sendStatus(204);
});
If you prefer modern reporting, use report-to, but report-uri is still handy and widely understood.
Common Foundation CSP breakages
1. Inline initializer scripts
Blocked because script-src 'self' doesn’t allow inline code.
Fix: move code into a file or add a nonce.
2. Inline style="" attributes from widgets
Blocked because style-src 'self' doesn’t allow inline styles.
Fix: remove inline styles, refactor widget output, or temporarily allow 'unsafe-inline'.
3. CDN-hosted assets not whitelisted
Blocked because CSP only trusts 'self'.
Fix: add the exact CDN origin to script-src, style-src, or font-src.
4. Embedded forms or consent dialogs failing
Usually frame-src or connect-src.
Fix: inspect the browser console and add the required origins narrowly.
A stricter production template for most Foundation sites
If you self-host everything and avoid inline code, this is a good default:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
And if you need one inline script for bootstrapping:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-RUNTIME_NONCE';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
upgrade-insecure-requests;
That’s the sweet spot for a lot of Foundation deployments: strict enough to matter, boring enough to maintain.
Final practical advice
If you’re starting fresh with Foundation, do these three things:
- Self-host Foundation, jQuery, and what-input.
- Put all JS initializers in external files.
- Refuse to add
'unsafe-inline'toscript-src.
That alone saves you from most CSP pain.
The second you bring in GTM, consent managers, A/B testing tools, and random embeds, your policy stops being elegant. That’s normal. The goal isn’t purity. The goal is reducing exploitability without making your app impossible to run.
That’s what good CSP work looks like in practice.