This commit is contained in:
Mike Cao committed 2026-07-28 14:07:19 -07:00
commit b5d27620e7
3 files changed
+45 -26

No files matched your search

+15 -3
View File
@@ -1,4 +1,5 @@
import { type NextRequest, NextResponse } from 'next/server';
import { getContentSecurityPolicy } from '@/lib/csp';
import { matchesConfiguredPath } from '@/lib/match-configured-path';
export const config = {
@@ -23,6 +24,9 @@ const trackerHeaders = {
'Cache-Control': 'public, max-age=86400, must-revalidate',
};
// Resolved once at startup — env vars don't change after the process starts.
const contentSecurityPolicy = getContentSecurityPolicy();
function customCollectEndpoint(request: NextRequest) {
const collectEndpoint = process.env.COLLECT_API_ENDPOINT;
@@ -69,12 +73,20 @@ function disableLogin(request: NextRequest) {
export default function middleware(req: NextRequest) {
const fns = [customCollectEndpoint, customScriptName, customScriptUrl, disableLogin];
let res: NextResponse | undefined;
for (const fn of fns) {
const res = fn(req);
res = fn(req);
if (res) {
return res;
break;
}
}
return NextResponse.next();
res ??= NextResponse.next();
// Set the CSP here, not only at build time in next.config.ts, so
// ALLOWED_FRAME_URLS is resolved from the runtime environment.
res.headers.set('Content-Security-Policy', contentSecurityPolicy);
return res;
}
+2 -23
View File
@@ -1,6 +1,7 @@
import 'dotenv/config';
import createNextIntlPlugin from 'next-intl/plugin';
import pkg from './package.json' with { type: 'json' };
import { getContentSecurityPolicy } from './src/lib/csp';
const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts');
@@ -17,20 +18,11 @@ const corsMaxAge = process.env.CORS_MAX_AGE || '';
const defaultCurrency = process.env.DEFAULT_CURRENCY || '';
const defaultLocale = process.env.DEFAULT_LOCALE || '';
const forceSSL = process.env.FORCE_SSL || '';
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
const trackerScriptName = process.env.TRACKER_SCRIPT_NAME || '';
const trackerScriptURL = process.env.TRACKER_SCRIPT_URL || '';
const selfTrack = process.env.UMAMI_SELF_TRACK || '';
const selfRecord = process.env.UMAMI_SELF_RECORD || '';
function getUrlOrigin(url: string) {
try {
return new URL(url).origin;
} catch {
return '';
}
}
function isRelativeUrl(url: string) {
return Boolean(url && !/^https?:\/\//i.test(url));
}
@@ -39,19 +31,6 @@ function normalizePath(url: string) {
return `/${url.replace(/^\/+|\/+$/g, '')}`;
}
const apiUrlOrigin = getUrlOrigin(apiUrl);
const connectSrc = ["'self'", 'https:', apiUrlOrigin].filter(Boolean).join(' ');
const contentSecurityPolicy = `
default-src 'self';
img-src 'self' https: data: blob:;
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src ${connectSrc};
frame-src 'self' http: https:;
frame-ancestors 'self' ${frameAncestors};
`;
const defaultHeaders = [
{
key: 'X-DNS-Prefetch-Control',
@@ -59,7 +38,7 @@ const defaultHeaders = [
},
{
key: 'Content-Security-Policy',
value: contentSecurityPolicy.replace(/\s{2,}/g, ' ').trim(),
value: getContentSecurityPolicy(),
},
];
+28
View File
@@ -0,0 +1,28 @@
function getUrlOrigin(url: string) {
try {
return new URL(url).origin;
} catch {
return '';
}
}
// Builds the Content-Security-Policy. Reads the environment when called, so it
// can run at build time (next.config.ts) or per request in the Docker proxy,
// where ALLOWED_FRAME_URLS and API_URL then apply without rebuilding the image.
export function getContentSecurityPolicy() {
const apiUrlOrigin = getUrlOrigin(process.env.API_URL || '');
const connectSrc = ["'self'", 'https:', apiUrlOrigin].filter(Boolean).join(' ');
const frameAncestors = ["'self'", process.env.ALLOWED_FRAME_URLS || ''].filter(Boolean).join(' ');
const directives = [
`default-src 'self'`,
`img-src 'self' https: data: blob:`,
`script-src 'self' 'unsafe-eval' 'unsafe-inline'`,
`style-src 'self' 'unsafe-inline'`,
`connect-src ${connectSrc}`,
`frame-src 'self' http: https:`,
`frame-ancestors ${frameAncestors}`,
];
return `${directives.join('; ')};`;
}