Skip to content

Commit fe2c704

Browse files
committed
fix(build): resolve CSP nonce conflicts causing blank page on refresh
- Implement shared nonce system across build optimizers - Fix CSS regex escaping preventing builds - Disable conflicting Vite CSP plugin in production - Ensure consistent base64 nonce format across all inline content Fixes blank page issues from CSP violations due to mismatched nonces.
1 parent 11bfe53 commit fe2c704

File tree

7 files changed

+103
-16
lines changed

7 files changed

+103
-16
lines changed

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"scripts": {
2525
"dev": "vite",
2626
"start": "vite",
27-
"build": "vite build && cp -r ../docs/* dist/docs/ && cp public/maintenance.html dist/maintenance.html && node scripts/advanced-post-build-optimize.cjs && node scripts/optimize-images.cjs && node scripts/optimize-css.cjs && node scripts/optimize-accessibility.cjs && node scripts/gtmetrix-optimizer.cjs && node scripts/bundle-optimizer.cjs && node ../scripts/fix-css-compatibility.js",
27+
"build": "vite build && cp -r ../docs/* dist/docs/ && cp public/maintenance.html dist/maintenance.html && node scripts/optimize-images.cjs && node scripts/optimize-css.cjs && node scripts/optimize-accessibility.cjs && node scripts/gtmetrix-optimizer.cjs && node scripts/advanced-post-build-optimize.cjs && node scripts/bundle-optimizer.cjs && node ../scripts/fix-css-compatibility.js",
2828
"build:fast": "vite build && cp -r ../docs/* dist/docs/ && cp public/maintenance.html dist/maintenance.html && node scripts/post-build-optimize.cjs",
2929
"build:analyze": "ANALYZE=true vite build",
3030
"clean": "rm -rf dist .vite",

frontend/public/_headers

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# Netlify Headers Configuration - Not YAML format
22
/*
3-
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.thinkred.tech https://script.google.com https://script.googleusercontent.com; object-src 'none'; media-src 'self'; child-src 'none'; frame-src 'none'; worker-src 'self'; manifest-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self' https://script.google.com
43
Cross-Origin-Opener-Policy: same-origin
54
Cross-Origin-Embedder-Policy: require-corp
65
X-Content-Type-Options: nosniff

frontend/scripts/advanced-post-build-optimize.cjs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
const fs = require('fs');
1111
const path = require('path');
12-
const crypto = require('crypto');
12+
const { getOrCreateNonce } = require('./nonce-generator.cjs');
1313

1414
const distDir = path.join(__dirname, '..', 'dist');
1515
const indexPath = path.join(distDir, 'index.html');
@@ -18,8 +18,8 @@ const assetsDir = path.join(distDir, 'assets');
1818
console.log('🚀 Starting Advanced Performance Optimization...');
1919

2020
try {
21-
// Generate unique nonce for CSP
22-
const nonce = crypto.randomBytes(16).toString('base64');
21+
// Use shared nonce generator
22+
const nonce = getOrCreateNonce();
2323

2424
// Read the index.html file
2525
let html = fs.readFileSync(indexPath, 'utf8');
@@ -248,16 +248,16 @@ try {
248248
// 4. ENHANCED CSP WITH NONCE - Replace existing CSP if present
249249
const cspMeta = `<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'nonce-${nonce}' https://script.google.com https://script.googleusercontent.com; style-src 'self' 'nonce-${nonce}' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://script.google.com https://script.googleusercontent.com; frame-src 'self' https://script.google.com; object-src 'none'; base-uri 'self'; form-action 'self';">`;
250250

251-
// Remove any existing CSP headers to prevent conflicts
252-
html = html.replace(/<meta[^>]*Content-Security-Policy[^>]*>/gi, '');
251+
// Remove any existing CSP headers to prevent conflicts - more comprehensive patterns
252+
html = html.replace(/<meta[^>]*http-equiv=["']?Content-Security-Policy["']?[^>]*>/gi, '');
253+
html = html.replace(/<meta[^>]*content=["'][^"']*Content-Security-Policy[^"']*["'][^>]*>/gi, '');
253254

254255
// 5. ADDITIONAL SECURITY AND PERFORMANCE HEADERS
255256
const securityMeta = `
256257
<meta http-equiv="X-Content-Type-Options" content="nosniff">
257-
<meta http-equiv="X-Frame-Options" content="SAMEORIGIN">
258258
<meta http-equiv="X-XSS-Protection" content="1; mode=block">
259259
<meta http-equiv="Referrer-Policy" content="strict-origin-when-cross-origin">
260-
<meta http-equiv="Permissions-Policy" content="camera=(), microphone=(), geolocation=(), payment=()">
260+
<meta http-equiv="Permissions-Policy" content="camera=(), microphone=(), geolocation=(), payment=(), fullscreen=(self)">
261261
`;
262262

263263
// 6. SERVICE WORKER REGISTRATION
@@ -278,6 +278,9 @@ try {
278278
// Remove existing critical CSS and replace with optimized version
279279
html = html.replace(/<style[^>]*>[\s\S]*?<\/style>/g, '');
280280

281+
// Replace nonce placeholders with actual nonce
282+
html = html.replace(/__CSP_NONCE__/g, nonce);
283+
281284
// Find the head tag and insert optimized content
282285
const headEndIndex = html.indexOf('</head>');
283286
if (headEndIndex !== -1) {

frontend/scripts/gtmetrix-optimizer.cjs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
const fs = require('fs');
2222
const path = require('path');
23+
const { getOrCreateNonce } = require('./nonce-generator.cjs');
2324

2425
const distDir = path.join(process.cwd(), 'dist');
2526
const assetsDir = path.join(distDir, 'assets');
@@ -220,9 +221,12 @@ function deferOffscreenImages() {
220221

221222
let html = fs.readFileSync(indexPath, 'utf8');
222223

224+
// Get shared nonce for script tag
225+
const nonce = getOrCreateNonce();
226+
223227
// Add intersection observer for images
224228
const lazyLoadScript = `
225-
<script>
229+
<script nonce="${nonce}">
226230
(function() {
227231
'use strict';
228232
@@ -392,10 +396,18 @@ function additionalOptimizations() {
392396
@media(max-width:768px){.text-4xl{font-size:1.875rem}.text-xl{font-size:1.125rem}}
393397
</style>`;
394398

395-
// Insert critical CSS in head
396-
const firstLinkIndex = html.indexOf('<link');
397-
if (firstLinkIndex !== -1) {
398-
html = html.slice(0, firstLinkIndex) + criticalCSS + '\n ' + html.slice(firstLinkIndex);
399+
// Insert critical CSS after the last meta tag but before the first link/style/script
400+
const lastMetaIndex = html.lastIndexOf('</meta>') !== -1 ? html.lastIndexOf('</meta>') : html.lastIndexOf('<meta');
401+
if (lastMetaIndex !== -1) {
402+
// Find the end of the last meta tag
403+
const insertIndex = html.indexOf('>', lastMetaIndex) + 1;
404+
html = html.slice(0, insertIndex) + criticalCSS + '\n ' + html.slice(insertIndex);
405+
} else {
406+
// Fallback: insert before first link
407+
const firstLinkIndex = html.indexOf('<link');
408+
if (firstLinkIndex !== -1) {
409+
html = html.slice(0, firstLinkIndex) + criticalCSS + '\n ' + html.slice(firstLinkIndex);
410+
}
399411
}
400412

401413
fs.writeFileSync(indexPath, html);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Shared nonce generator for build optimization scripts
5+
* Ensures consistent nonce across all optimizers
6+
*/
7+
8+
const crypto = require('crypto');
9+
const fs = require('fs');
10+
const path = require('path');
11+
12+
const NONCE_FILE = path.join(__dirname, '..', 'dist', '.build-nonce');
13+
14+
/**
15+
* Generate or retrieve existing nonce for this build
16+
* @returns {string} Base64 encoded nonce
17+
*/
18+
function getOrCreateNonce() {
19+
// Check if nonce already exists for this build
20+
if (fs.existsSync(NONCE_FILE)) {
21+
try {
22+
const existingNonce = fs.readFileSync(NONCE_FILE, 'utf8').trim();
23+
if (existingNonce && existingNonce.length > 0) {
24+
return existingNonce;
25+
}
26+
} catch (error) {
27+
// If we can't read the existing nonce, generate a new one
28+
}
29+
}
30+
31+
// Generate new nonce
32+
const nonce = crypto.randomBytes(16).toString('base64');
33+
34+
// Store nonce for other scripts to use
35+
try {
36+
fs.writeFileSync(NONCE_FILE, nonce, 'utf8');
37+
} catch (error) {
38+
console.warn('Warning: Could not save nonce to file:', error.message);
39+
}
40+
41+
return nonce;
42+
}
43+
44+
/**
45+
* Clean up nonce file after build
46+
*/
47+
function cleanupNonce() {
48+
try {
49+
if (fs.existsSync(NONCE_FILE)) {
50+
fs.unlinkSync(NONCE_FILE);
51+
}
52+
} catch (error) {
53+
// Ignore cleanup errors
54+
}
55+
}
56+
57+
module.exports = {
58+
getOrCreateNonce,
59+
cleanupNonce
60+
};
61+
62+
// If called directly, output the nonce
63+
if (require.main === module) {
64+
console.log(getOrCreateNonce());
65+
}

frontend/scripts/optimize-css.cjs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,14 @@ function isCriticalSelector(selector) {
7171
if (critical.endsWith('-')) {
7272
return selector.includes(critical);
7373
}
74-
return selector.includes(critical) || selector.match(new RegExp(critical.replace(/\\/g, '')));
74+
try {
75+
// Escape special regex characters to prevent invalid regex errors
76+
const escapedCritical = critical.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
77+
return selector.includes(critical) || selector.match(new RegExp(escapedCritical));
78+
} catch {
79+
// Fallback to simple string match if regex fails
80+
return selector.includes(critical);
81+
}
7582
});
7683
}
7784

frontend/vite.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ export default defineConfig({
2020
}
2121
}),
2222
// CSP Plugin for secure Content Security Policy - Addresses GitHub Issue #45
23+
// Note: Disabled during build - post-build optimizers handle CSP nonces
2324
createCSPPlugin({
24-
enabled: true,
25+
enabled: process.env.NODE_ENV !== "production",
2526
development: process.env.NODE_ENV !== "production",
2627
}),
2728
],

0 commit comments

Comments
 (0)