mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-10-15 05:11:26 +00:00
fix: resolve security vulnerabilities in whitelabeling
- Sanitize customCss in _document.tsx to prevent XSS breakouts by stripping </style> tags. - Remove SSR favicon fetching to prevent SSRF and internal network scanning, falling back to client-side fetching.
This commit is contained in:
@@ -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<string, { href: string; expiresAt: number }>;
|
||||
}
|
||||
|
||||
const faviconCache =
|
||||
globalThis.__FAVICON_CACHE ||
|
||||
new Map<string, { href: string; expiresAt: number }>();
|
||||
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<string | null> {
|
||||
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<DocumentInitialProps & WhitelabelingDocumentProps> => {
|
||||
@@ -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 </style> 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)
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user