Revert "perf: make root HTML cacheable by dropping per-request CSP nonce"
This reverts commit 522bf6a019.
This commit is contained in:
@@ -180,7 +180,7 @@ Built with Next.js and the JMAP protocol.
|
|||||||
- **TOTP two-factor authentication**
|
- **TOTP two-factor authentication**
|
||||||
- **Account security panel** - manage passwords and 2FA via Stalwart admin API
|
- **Account security panel** - manage passwords and 2FA via Stalwart admin API
|
||||||
- **"Remember me"** - AES-256-GCM encrypted httpOnly cookie (opt-in)
|
- **"Remember me"** - AES-256-GCM encrypted httpOnly cookie (opt-in)
|
||||||
- **Security headers** - enforced CSP (script-src 'self', no unsafe-inline), X-Frame-Options, Referrer-Policy; SSRF redirect validation; PDF iframe sandbox; IP spoofing prevention
|
- **Security headers** - enforced CSP with per-request nonce, X-Frame-Options, Referrer-Policy; SSRF redirect validation; PDF iframe sandbox; IP spoofing prevention
|
||||||
- **Plugin hardening** - dangerous-pattern detection, admin approval required, secure HTTP proxy API (no auth-header exposure)
|
- **Plugin hardening** - dangerous-pattern detection, admin approval required, secure HTTP proxy API (no auth-header exposure)
|
||||||
- **Newsletter unsubscribe** (RFC 2369)
|
- **Newsletter unsubscribe** (RFC 2369)
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,15 @@ export default async function LocaleLayout({
|
|||||||
|
|
||||||
if (!(locales as readonly string[]).includes(locale)) notFound();
|
if (!(locales as readonly string[]).includes(locale)) notFound();
|
||||||
|
|
||||||
|
let messages;
|
||||||
|
try {
|
||||||
|
messages = (await import(`@/locales/${locale}/common.json`)).default;
|
||||||
|
} catch {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider locale={locale}>
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<CalendarAlertProvider>
|
<CalendarAlertProvider>
|
||||||
<RateLimitToastProvider>
|
<RateLimitToastProvider>
|
||||||
|
|||||||
+22
-1
@@ -1,5 +1,6 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import { headers } from "next/headers";
|
||||||
import { getLocale } from "next-intl/server";
|
import { getLocale } from "next-intl/server";
|
||||||
import { PWAInstallPrompt } from "@/components/pwa-install-prompt";
|
import { PWAInstallPrompt } from "@/components/pwa-install-prompt";
|
||||||
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
|
import { ServiceWorkerRegistration } from "@/components/service-worker-registration";
|
||||||
@@ -39,6 +40,7 @@ export default async function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
|
const nonce = (await headers()).get("x-nonce") ?? "";
|
||||||
const parentOrigin = process.env.NEXT_PUBLIC_PARENT_ORIGIN || "";
|
const parentOrigin = process.env.NEXT_PUBLIC_PARENT_ORIGIN || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -55,7 +57,26 @@ export default async function RootLayout({
|
|||||||
{parentOrigin && (
|
{parentOrigin && (
|
||||||
<meta name="parent-origin" content={parentOrigin} />
|
<meta name="parent-origin" content={parentOrigin} />
|
||||||
)}
|
)}
|
||||||
<script src="/theme-init.js" />
|
<script
|
||||||
|
nonce={nonce}
|
||||||
|
suppressHydrationWarning
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem('theme-storage');
|
||||||
|
const theme = stored ? JSON.parse(stored).state.theme : 'system';
|
||||||
|
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||||
|
const resolved = theme === 'system' ? systemTheme : theme;
|
||||||
|
document.documentElement.classList.remove('light', 'dark');
|
||||||
|
document.documentElement.classList.add(resolved);
|
||||||
|
} catch (e) {
|
||||||
|
document.documentElement.classList.add('light');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const ALL_MESSAGES = {
|
|||||||
|
|
||||||
interface IntlProviderProps {
|
interface IntlProviderProps {
|
||||||
locale: string;
|
locale: string;
|
||||||
|
messages: Record<string, unknown>;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { routing } from "./i18n/routing";
|
|||||||
const intlMiddleware = createIntlMiddleware(routing);
|
const intlMiddleware = createIntlMiddleware(routing);
|
||||||
|
|
||||||
export function proxy(request: NextRequest) {
|
export function proxy(request: NextRequest) {
|
||||||
|
const nonce = crypto.randomUUID();
|
||||||
const isDev = process.env.NODE_ENV === "development";
|
const isDev = process.env.NODE_ENV === "development";
|
||||||
|
|
||||||
const scriptSrc = isDev
|
const scriptSrc = isDev
|
||||||
? `'self' 'unsafe-eval' blob:`
|
? `'self' 'nonce-${nonce}' 'unsafe-eval' blob:`
|
||||||
: `'self' blob:`;
|
: `'self' 'nonce-${nonce}' blob:`;
|
||||||
|
|
||||||
const connectSrc = isDev ? `'self' https: ws: wss:` : `'self' https:`;
|
const connectSrc = isDev ? `'self' https: ws: wss:` : `'self' https:`;
|
||||||
|
|
||||||
@@ -51,6 +52,13 @@ export function proxy(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
const response = intlResponse ?? NextResponse.next();
|
const response = intlResponse ?? NextResponse.next();
|
||||||
|
|
||||||
|
const existing = response.headers.get("x-middleware-override-headers");
|
||||||
|
response.headers.set(
|
||||||
|
"x-middleware-override-headers",
|
||||||
|
existing ? `${existing},x-nonce` : "x-nonce"
|
||||||
|
);
|
||||||
|
response.headers.set("x-middleware-request-x-nonce", nonce);
|
||||||
|
|
||||||
response.headers.set("X-Content-Type-Options", "nosniff");
|
response.headers.set("X-Content-Type-Options", "nosniff");
|
||||||
|
|
||||||
// X-Frame-Options only supports DENY/SAMEORIGIN. When frame-ancestors
|
// X-Frame-Options only supports DENY/SAMEORIGIN. When frame-ancestors
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
(function () {
|
|
||||||
try {
|
|
||||||
var stored = localStorage.getItem('theme-storage');
|
|
||||||
var theme = stored ? JSON.parse(stored).state.theme : 'system';
|
|
||||||
var systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
||||||
var resolved = theme === 'system' ? systemTheme : theme;
|
|
||||||
document.documentElement.classList.remove('light', 'dark');
|
|
||||||
document.documentElement.classList.add(resolved);
|
|
||||||
} catch (e) {
|
|
||||||
document.documentElement.classList.add('light');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
Reference in New Issue
Block a user