diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index bfe175c80..2046b6f9f 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -14,70 +14,7 @@ interface WhitelabelingDocumentProps { customCss: string | null; } -// Cache the resolved favicon (inlined as a data URI) so we don't re-fetch the -// remote image on every server render. Keyed by the configured favicon URL. -const FAVICON_CACHE_TTL = 60 * 60 * 1000; // 1 hour -const SETTINGS_CACHE_TTL = 60 * 1000; // 1 minute - -declare global { - var __SETTINGS_CACHE: { - data: { - metaTitle: string | null; - faviconHref: string | null; - customCss: string | null; - }; - expiresAt: number; - } | null; - var __FAVICON_CACHE: Map; -} - -const faviconCache = - globalThis.__FAVICON_CACHE || - new Map(); -globalThis.__FAVICON_CACHE = faviconCache; - -/** - * Resolve the favicon to an inline data URI so it is present in the initial - * HTML and renders without a network round-trip (no flash of the default - * favicon). Falls back to the raw URL if the image can't be fetched. - */ -async function resolveFaviconHref( - faviconUrl: string | null | undefined, -): Promise { - if (!faviconUrl) return null; - - const cached = faviconCache.get(faviconUrl); - if (cached && cached.expiresAt > Date.now()) { - return cached.href; - } - - // Default to the raw URL so the custom favicon still loads if inlining fails. - let href = faviconUrl; - try { - const controller = new AbortController(); - const timeout = setTimeout(() => controller.abort(), 3000); - const response = await fetch(faviconUrl, { signal: controller.signal }); - clearTimeout(timeout); - - if (response.ok) { - const buffer = Buffer.from(await response.arrayBuffer()); - // Avoid embedding very large images directly in the HTML. - if (buffer.byteLength <= 512 * 1024) { - const contentType = response.headers.get("content-type") || "image/png"; - href = `data:${contentType};base64,${buffer.toString("base64")}`; - } - } - } catch { - // Keep the raw URL fallback. - } - - faviconCache.set(faviconUrl, { - href, - expiresAt: Date.now() + FAVICON_CACHE_TTL, - }); - return href; -} export default function Document({ metaTitle, @@ -108,6 +45,19 @@ export default function Document({ ); } +const SETTINGS_CACHE_TTL = 60 * 1000; // 1 minute + +declare global { + var __SETTINGS_CACHE: { + data: { + metaTitle: string | null; + faviconHref: string | null; + customCss: string | null; + }; + expiresAt: number; + } | null; +} + Document.getInitialProps = async ( ctx: DocumentContext, ): Promise => { @@ -132,8 +82,9 @@ Document.getInitialProps = async ( const config = await getPublicWhitelabelingConfig(); if (config) { metaTitle = config.metaTitle; - customCss = config.customCss; - faviconHref = await resolveFaviconHref(config.faviconUrl); + // Remove any tags to prevent XSS breakout + customCss = config.customCss ? config.customCss.replace(/<\/style>/gi, "") : null; + faviconHref = config.faviconUrl || null; } } catch { // Fall back to defaults if settings can't be read (e.g. DB not ready) diff --git a/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts b/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts index 6bc0d0f8f..bc38c8ff1 100644 --- a/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts +++ b/apps/dokploy/server/api/routers/proprietary/whitelabeling.ts @@ -17,9 +17,6 @@ import { /** Invalidate the SSR branding caches in _document.tsx so the next request picks up fresh settings. */ function clearBrandingSSRCache() { globalThis.__SETTINGS_CACHE = null; - if (globalThis.__FAVICON_CACHE) { - globalThis.__FAVICON_CACHE.clear(); - } } export const whitelabelingRouter = createTRPCRouter({