From a6e6341e232de99b5128d55824f7ce2a7b83dff8 Mon Sep 17 00:00:00 2001 From: Nyk <0xnykcd@googlemail.com> Date: Fri, 13 Mar 2026 12:18:24 +0700 Subject: [PATCH] =?UTF-8?q?fix(auth):=20fix=20HTTP/Tailscale=20login=20?= =?UTF-8?q?=E2=80=94=20opt-in=20HTTPS=20redirect,=20CSP=20nonce=20propagat?= =?UTF-8?q?ion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace unconditional HTTP→HTTPS redirect with opt-in via NEXT_PUBLIC_FORCE_HTTPS=1 - Propagate CSP nonce into forwarded request headers so SSR inline scripts get the nonce - Bind nonce attribute to layout's inline theme script to prevent CSP violations - Extract CSP and browser-security helpers into dedicated modules with tests Closes #308, #309, #311 --- src/app/[[...panel]]/page.tsx | 13 ++++---- src/app/layout.tsx | 6 +++- src/lib/__tests__/browser-security.test.ts | 36 ++++++++++++++++++++++ src/lib/__tests__/csp.test.ts | 24 +++++++++++++++ src/lib/browser-security.ts | 22 +++++++++++++ src/lib/csp.ts | 31 +++++++++++++++++++ src/proxy.ts | 27 +++++----------- 7 files changed, 132 insertions(+), 27 deletions(-) create mode 100644 src/lib/__tests__/browser-security.test.ts create mode 100644 src/lib/__tests__/csp.test.ts create mode 100644 src/lib/browser-security.ts create mode 100644 src/lib/csp.ts diff --git a/src/app/[[...panel]]/page.tsx b/src/app/[[...panel]]/page.tsx index 308a52d..16e7f23 100644 --- a/src/app/[[...panel]]/page.tsx +++ b/src/app/[[...panel]]/page.tsx @@ -38,6 +38,7 @@ import { ExecApprovalPanel } from '@/components/panels/exec-approval-panel' import { ChatPagePanel } from '@/components/panels/chat-page-panel' import { ChatPanel } from '@/components/chat/chat-panel' import { getPluginPanel } from '@/lib/plugins' +import { shouldRedirectDashboardToHttps } from '@/lib/browser-security' import { ErrorBoundary } from '@/components/ErrorBoundary' import { LocalModeBanner } from '@/components/layout/local-mode-banner' import { UpdateBanner } from '@/components/layout/update-banner' @@ -65,10 +66,6 @@ function renderPluginPanel(panelId: string) { return pluginPanel ? createElement(pluginPanel) : } -function isLocalHost(hostname: string): boolean { - return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1' -} - export default function Home() { const router = useRouter() const { connect } = useWebSocket() @@ -148,9 +145,11 @@ export default function Home() { useEffect(() => { setIsClient(true) - // OpenClaw control-ui device identity requires a secure browser context. - // Redirect remote HTTP sessions to HTTPS automatically to avoid handshake failures. - if (window.location.protocol === 'http:' && !isLocalHost(window.location.hostname)) { + if (shouldRedirectDashboardToHttps({ + protocol: window.location.protocol, + hostname: window.location.hostname, + forceHttps: process.env.NEXT_PUBLIC_FORCE_HTTPS === '1', + })) { const secureUrl = new URL(window.location.href) secureUrl.protocol = 'https:' window.location.replace(secureUrl.toString()) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b585218..ed9ee18 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata, Viewport } from 'next' import { Inter, JetBrains_Mono } from 'next/font/google' +import { headers } from 'next/headers' import { ThemeProvider } from 'next-themes' import { THEME_IDS } from '@/lib/themes' import { ThemeBackground } from '@/components/ui/theme-background' @@ -78,17 +79,20 @@ export const metadata: Metadata = { }, } -export default function RootLayout({ +export default async function RootLayout({ children, }: { children: React.ReactNode }) { + const nonce = (await headers()).get('x-nonce') || undefined + return ( {/* Blocking script to set 'dark' class before first paint, preventing FOUC. Content is a static string literal — no user input, no XSS vector. */}