fix(routing): allow hash path redirect under CSP

This commit is contained in:
Tommaso Casaburi
2026-04-29 16:23:13 +07:00
parent 0f5f764b8b
commit fe48208078
3 changed files with 53 additions and 2 deletions
+1 -1
View File
@@ -65,8 +65,8 @@
});
}
</script>
<!-- Normalize path-based URLs before HashRouter starts. -->
<script>
// Redirect non-hash URLs before the app bundle is requested.
if (
window.location.protocol !== 'file:' &&
window.location.pathname &&
+1 -1
View File
@@ -43,7 +43,7 @@
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; base-uri 'none'; object-src 'none'; form-action 'self'; frame-ancestors 'none'; script-src 'self' 'wasm-unsafe-eval' 'sha256-kF+BgNpp0e7WM8m8a9eRaLHmLrSQGQVmaFYjvPYU1/8=' 'sha256-kiz84y15tsP8n+Qo/YHYAPLBR6nQyVSqHO4BWcVVOik=' https://platform.twitter.com https://embed.reddit.com https://www.tiktok.com https://www.instagram.com; style-src 'self' 'unsafe-inline'; img-src 'self' https: http: data: blob:; media-src 'self' https: http: blob:; connect-src 'self' https: http: wss: ws: blob:; frame-src https: http://localhost:* http://*.localhost:* http://127.0.0.1:* http://[::1]:*; child-src https: http://localhost:* http://*.localhost:* http://127.0.0.1:* http://[::1]:*; worker-src 'self' blob:; manifest-src 'self'"
"value": "default-src 'self'; base-uri 'none'; object-src 'none'; form-action 'self'; frame-ancestors 'none'; script-src 'self' 'wasm-unsafe-eval' 'sha256-kF+BgNpp0e7WM8m8a9eRaLHmLrSQGQVmaFYjvPYU1/8=' 'sha256-uIRmdbN2zhK9eOgX0W2eTHZJSiH88uiZBeEZTwCIUyI=' https://platform.twitter.com https://embed.reddit.com https://www.tiktok.com https://www.instagram.com; style-src 'self' 'unsafe-inline'; img-src 'self' https: http: data: blob:; media-src 'self' https: http: blob:; connect-src 'self' https: http: wss: ws: blob:; frame-src https: http://localhost:* http://*.localhost:* http://127.0.0.1:* http://[::1]:*; child-src https: http://localhost:* http://*.localhost:* http://127.0.0.1:* http://[::1]:*; worker-src 'self' blob:; manifest-src 'self'"
},
{ "key": "X-DNS-Prefetch-Control", "value": "off" }
]
+51
View File
@@ -2,6 +2,7 @@ import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { resolve } from 'path';
import { readFileSync } from 'fs';
import { createHash } from 'crypto';
import { VitePWA } from 'vite-plugin-pwa';
const { version: packageVersion } = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
@@ -97,6 +98,55 @@ function appVersionMetadataPlugin() {
};
}
function getVercelContentSecurityPolicy() {
const vercelConfig = JSON.parse(readFileSync(new URL('./vercel.json', import.meta.url), 'utf8'));
const cspHeader = vercelConfig.headers
?.flatMap((entry) => entry.headers || [])
.find((header) => typeof header.key === 'string' && header.key.toLowerCase() === 'content-security-policy');
if (typeof cspHeader?.value !== 'string') {
throw new Error('vercel.json is missing a Content-Security-Policy header.');
}
return cspHeader.value;
}
function getInlineScriptHashes(indexHtml) {
const scriptPattern = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
const hashes = [];
for (const [, attributes, source] of indexHtml.matchAll(scriptPattern)) {
if (/\bsrc\s*=/i.test(attributes) || source.trim().length === 0) {
continue;
}
hashes.push(`sha256-${createHash('sha256').update(source).digest('base64')}`);
}
return [...new Set(hashes)];
}
function verifyVercelCspHashesPlugin() {
return {
name: 'fivechan-verify-vercel-csp-hashes',
apply: 'build',
enforce: 'post',
closeBundle() {
const indexHtml = readFileSync(new URL(`./${buildOutDir}/index.html`, import.meta.url), 'utf8');
const contentSecurityPolicy = getVercelContentSecurityPolicy();
const missingHashes = getInlineScriptHashes(indexHtml).filter(
(hash) => !contentSecurityPolicy.includes(`'${hash}'`) && !contentSecurityPolicy.includes(hash),
);
if (missingHashes.length > 0) {
const plural = missingHashes.length === 1 ? '' : 'es';
throw new Error(`vercel.json Content-Security-Policy is missing inline script hash${plural}: ${missingHashes.join(', ')}`);
}
},
};
}
function adaptReactPluginForRolldown(plugin) {
if (!plugin?.config || plugin.name !== 'vite:react-babel') {
return plugin;
@@ -252,6 +302,7 @@ export default defineConfig({
],
},
}),
verifyVercelCspHashesPlugin(),
],
resolve: {
alias: {