This document explains the CSP security improvements implemented to prevent XSS (Cross-Site Scripting) attacks across the Protocol Guide application.
scriptSrc: ["'self'", "'unsafe-inline'"], // VULNERABLE to XSS
styleSrc: ["'self'", "'unsafe-inline'"], // VULNERABLE to CSS injection
imgSrc: ["'self'", "data:", "https:", "blob:"], // Too permissive
fontSrc: ["'self'", "data:", "https:"], // Too permissivescriptSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.cspNonce}'`, // ✅ Nonce-based CSP
ENV.isProduction ? "" : "'unsafe-eval'", // Only in dev for HMR
],
styleSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.cspNonce}'`, // ✅ Nonce-based CSP
],
imgSrc: [
"'self'",
"data:",
"blob:",
"https://*.supabase.co", // ✅ Restricted to Supabase only
],
fontSrc: [
"'self'",
"data:",
"https://fonts.gstatic.com", // ✅ Restricted to Google Fonts only
],
connectSrc: [
// ... existing origins
"https://protocol-guide-production.up.railway.app", // ✅ Added Railway backend
],style-src 'self' 'unsafe-inline'; # VULNERABLE
img-src 'self' data: https:; # Too permissive
font-src 'self' data:; # Missing trusted sourcesstyle-src 'self' 'sha256-oFgClRU4Ehoik1pRJieVXsCrbQAwssqUDdL4hnGQ6to='; # ✅ Hash-based CSP
img-src 'self' data: blob: https://*.supabase.co; # ✅ Restricted
font-src 'self' data: https://fonts.gstatic.com; # ✅ Restricted
connect-src ... https://protocol-guide-production.up.railway.app; # ✅ Added Railway-
Middleware generates unique nonce per request
app.use((req, res, next) => { res.locals.cspNonce = crypto.randomBytes(16).toString('base64'); next(); });
-
Helmet includes nonce in CSP header
scriptSrc: [ "'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`, ]
-
Add nonce to inline scripts/styles
<script nonce="<%= cspNonce %>"> console.log('Secure inline script'); </script>
For static HTML served by Netlify, we use SHA-256 hashes:
-
Calculate hash of inline style
echo -n "/* CSS content */" | openssl dgst -sha256 -binary | openssl base64
-
Add hash to CSP
style-src 'self' 'sha256-HASH_HERE'; -
HTML remains unchanged
<style id="expo-reset"> /* Inline styles work with hash-based CSP */ </style>
If you need to serve HTML from the backend server:
import { injectCspNonce } from '../_core/csp-utils';
app.get('/some-page', (req, res) => {
const html = `
<!DOCTYPE html>
<html>
<head>
<style>body { margin: 0; }</style>
</head>
<body>
<script>console.log('Hello');</script>
</body>
</html>
`;
// Automatically adds nonce attributes to inline scripts/styles
const secureHtml = injectCspNonce(html, res.locals.cspNonce);
res.send(secureHtml);
});If you need to add new inline styles to web/index.html:
-
Add the style to HTML
<style id="my-new-style"> /* Your CSS */ </style>
-
Calculate SHA-256 hash
echo -n "/* Your CSS */" | openssl dgst -sha256 -binary | openssl base64
-
Add hash to
netlify.tomlstyle-src 'self' 'sha256-EXISTING_HASH' 'sha256-NEW_HASH';
- ❌ 'unsafe-inline' allows ANY inline script/style → XSS vulnerability
- ❌ 'https:' allows images from ANY HTTPS domain → Data exfiltration risk
- ❌ Missing Railway backend → CORS errors, broken functionality
- ✅ Nonce-based CSP → Only whitelisted inline code executes
- ✅ Restricted domains → Images/fonts only from trusted sources
- ✅ Complete backend support → Railway backend properly whitelisted
- ✅ Hash-based CSP → Static HTML secured without server-side processing
Note: React Native Web currently requires 'unsafe-inline' and 'unsafe-eval' for script execution due to runtime code generation.
Current status:
- Backend (API): ✅ Fully secured with nonce-based CSP
- Frontend (Netlify):
⚠️ Scripts still use'unsafe-inline'(RN Web requirement) - Frontend (Netlify): ✅ Styles use hash-based CSP (more secure)
Future improvement: Consider migrating frontend to hash-based CSP for scripts by:
- Pre-compiling all React Native Web code
- Extracting inline scripts to external files
- Calculating hashes for remaining inline scripts
- Open browser DevTools → Console
- Look for CSP violations
- Should see:
[CSP] Blocked inline script execution - Should NOT see:
[CSP] Refused to load the script
- Should see:
# Check CSP headers
curl -I https://protocol-guide.com | grep -i content-security-policy
# Verify nonce in backend responses
curl -v http://localhost:3000/api/health 2>&1 | grep -i content-security-policy# Run security scanner
npx @jackfranklin/security-audit
# Check for CSP violations in production
# Use browser extension: CSP Evaluator or Security HeadersWhen adding new inline scripts/styles:
- Backend routes: Use
injectCspNonce()utility - Frontend HTML: Calculate hash and update
netlify.toml - External scripts: Add domain to
scriptSrcin CSP config - External images: Add domain to
imgSrcin CSP config
- Backend CSP headers configured with nonces
- Frontend CSP headers configured with hashes
- Railway backend URL added to
connectSrc - No CSP violations in browser console
- All images/fonts loading correctly
- API calls to Railway backend working
- Service worker loading correctly
- PWA installation working
For CSP-related issues:
- Check browser console for CSP violation reports
- Verify the violating resource URL
- Add to appropriate CSP directive if trusted
- Use hash or nonce for inline code
- 2026-01-23: Initial implementation of nonce-based and hash-based CSP
- Removed 'unsafe-inline' from backend
- Restricted img/font sources
- Added Railway backend support
- Created CSP utilities and documentation