Water.css is the kind of CSS framework I like for security work: tiny, boring, and mostly predictable. That matters for CSP because every extra build step, inline style hack, or third-party asset is another thing you need to allow.
If you’re using Water.css, your CSP can usually stay tight. Most setups only need to allow your own origin for styles, or a single CDN if you’re loading it remotely.
What Water.css changes in CSP
Water.css is just a stylesheet. In the normal case, CSP impact is limited to:
style-srcfor the stylesheet itselffont-srcif your page also loads custom fontsimg-srcif your content includes remote imagesstyle-src 'unsafe-inline'only if you add inline styles elsewhere
Water.css itself does not require:
- inline scripts
- eval
- remote script execution
- frames
- object/embed
- broad wildcard sources
That makes it easy to lock down.
Minimal CSP for self-hosted Water.css
If you downloaded water.min.css and serve it from your own site, start here:
Content-Security-Policy: default-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
This is a good baseline for a simple static site.
HTML example
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Water.css with CSP</title>
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'">
<link rel="stylesheet" href="/css/water.min.css">
</head>
<body>
<main>
<h1>Hello</h1>
<p>This page uses self-hosted Water.css.</p>
</main>
</body>
</html>
For production, send CSP as an HTTP response header, not a meta tag. Meta is okay for local testing, but I wouldn’t rely on it if I care about consistency.
CSP for Water.css from a CDN
If you load Water.css from a CDN, add that host to style-src.
Example:
Content-Security-Policy: default-src 'self'; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
HTML example
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/water.min.css">
That’s enough if Water.css is the only third-party asset.
If your site uses no JavaScript at all, I still like keeping script-src 'self' instead of omitting it. Explicit beats implicit.
Dark and light theme variants
Water.css often gets loaded as one of these:
<link rel="stylesheet" href="/css/water.min.css">
<link rel="stylesheet" href="/css/light.min.css">
<link rel="stylesheet" href="/css/dark.min.css">
CSP doesn’t care which file name you use. If they’re self-hosted, this stays the same:
Content-Security-Policy: default-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
If they come from a CDN, keep the CDN host in style-src.
If you use inline styles on top of Water.css
This is where people accidentally weaken CSP and then blame the CSS framework.
Water.css doesn’t force inline styles. But your app might have things like:
<div style="margin-top: 2rem;">Hello</div>
That requires 'unsafe-inline' in style-src, unless you move that CSS into a stylesheet or use a hash/nonce approach where supported.
Example policy that permits inline styles:
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
I try hard to avoid 'unsafe-inline' for styles, but in real projects it’s common. If this is temporary, make it temporary.
For directive details, the official CSP docs are still the source of truth: MDN CSP introduction. If you want short directive explanations, csp-guide.com is handy.
Recommended strict policy for a basic Water.css site
For a static marketing site or docs site using self-hosted Water.css and no third-party junk, I’d use something like this:
Content-Security-Policy:
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';
upgrade-insecure-requests;
Single-line version:
Content-Security-Policy: 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'; upgrade-insecure-requests
This is boring, and boring is good.
Nginx example
add_header Content-Security-Policy "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'; upgrade-insecure-requests" always;
Apache example
Header always set Content-Security-Policy "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'; upgrade-insecure-requests"
Express.js example
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
"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'; upgrade-insecure-requests"
);
next();
});
Water.css plus analytics and consent tools
This is where a clean Water.css setup turns into a normal modern website mess.
You gave a real 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-MGFlMWNmZjMtMzY1OC00ZjgwLTkzMGUtN2M2YTg0OGU1MTM4' '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 not “for Water.css”. It’s a policy for Water.css plus tag manager, analytics, Cookiebot, API calls, and a websocket.
That distinction matters. Don’t cargo-cult a huge CSP if all you need is one stylesheet.
When your Water.css site starts adding third parties
Use this pattern:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{RANDOM_NONCE}}' 'strict-dynamic' https://www.googletagmanager.com;
style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://www.google-analytics.com https://www.googletagmanager.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
If you don’t need inline styles, remove 'unsafe-inline'. If you don’t use a CDN for Water.css, remove https://cdn.jsdelivr.net.
Common mistakes
1. Allowing https: everywhere
I see this a lot:
Content-Security-Policy: default-src 'self' https:;
This is lazy and defeats a lot of the value of CSP. Water.css does not justify broad allowlists.
2. Putting CDN hosts in default-src only
If your stylesheet is remote, make sure it’s in style-src.
Bad:
Content-Security-Policy: default-src 'self' https://cdn.jsdelivr.net
Better:
Content-Security-Policy: default-src 'self'; style-src 'self' https://cdn.jsdelivr.net
3. Adding 'unsafe-inline' because “CSS is blocked”
Usually the problem is one inline style="" attribute or a <style> block you forgot about. Fix the source if you can.
4. Forgetting fonts
Water.css itself doesn’t need special font hosts if you stick to system fonts. But if your page loads webfonts, add the source under font-src.
Example:
Content-Security-Policy: default-src 'self'; style-src 'self'; font-src 'self' https://fonts.gstatic.com; script-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Only add that if you actually load fonts from there.
Good copy-paste policies
Self-hosted Water.css, no JS
Content-Security-Policy: default-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
Self-hosted Water.css, with local JS
Content-Security-Policy: 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'; frame-ancestors 'none'; form-action 'self'
CDN-hosted Water.css
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' https://cdn.jsdelivr.net; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
Water.css with inline styles allowed
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'; frame-ancestors 'none'; form-action 'self'
My default recommendation
If you can, self-host Water.css and keep the policy small:
Content-Security-Policy: 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'; upgrade-insecure-requests
That’s the sweet spot: strict enough to matter, simple enough to maintain, and not polluted by random vendors that have nothing to do with your CSS framework.