From f4b942984c2613fe2bccc81174383177c9a01fd4 Mon Sep 17 00:00:00 2001 From: WhiteX Date: Fri, 13 Jun 2025 23:19:30 +0300 Subject: [PATCH] feat: implement domain isolation system with middleware and loader scripts for enhanced security --- Dockerfile_coolify | 102 +++++++++++++++++---- apps/web/middleware.js | 49 ++++++++++ apps/web/pages/_document.js | 10 +- apps/web/public/domain-isolation-loader.js | 82 +++++++++++++++++ 4 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 apps/web/middleware.js create mode 100644 apps/web/public/domain-isolation-loader.js diff --git a/Dockerfile_coolify b/Dockerfile_coolify index b2512243..ecf82a7d 100644 --- a/Dockerfile_coolify +++ b/Dockerfile_coolify @@ -112,6 +112,9 @@ RUN chmod +x /app/start.sh RUN echo '#!/bin/bash\n\ echo "Generating runtime configuration..."\n\ mkdir -p /app/web/public\n\ +# Copy domain isolation loader to public folder\n\ +cp /app/web/public/domain-isolation-loader.js /app/web/public/ 2>/dev/null || echo "Domain isolation loader not found in source, generating it..."\n\ +# Generate runtime config\n\ cat > /app/web/public/runtime-config.js << EOF\n\ window.RUNTIME_CONFIG = {\n\ LEARNHOUSE_API_URL: "${NEXT_PUBLIC_LEARNHOUSE_API_URL:-}",\n\ @@ -125,43 +128,104 @@ EOF\n\ \n\ echo "Runtime configuration generated successfully"\n\ \n\ -# Create a utility script to override fetch for better URL control\n\ +# Create an enhanced utility script to override all network requests with domain isolation\n\ cat > /app/web/public/api-interceptor.js << EOF\n\ (function() {\n\ // Get the current domain\n\ const currentDomain = window.location.hostname;\n\ - console.log("Current domain:", currentDomain);\n\ + console.log("[Domain Isolation] Current domain:", currentDomain);\n\ \n\ // Check if RUNTIME_CONFIG is available\n\ if (!window.RUNTIME_CONFIG) {\n\ - console.error("Runtime config not found!");\n\ + console.warn("[Domain Isolation] Runtime config not found, creating empty one.");\n\ window.RUNTIME_CONFIG = {};\n\ }\n\ \n\ - // Save the original fetch function\n\ - const originalFetch = window.fetch;\n\ + // Store the domain info globally\n\ + window.LEARNHOUSE_DOMAIN = currentDomain;\n\ \n\ - // Override fetch to enforce current domain\n\ + // 1. Intercept fetch API\n\ + const originalFetch = window.fetch;\n\ window.fetch = function(url, options) {\n\ if (typeof url === "string") {\n\ - // Check if URL contains a domain that doesnt match current domain\n\ - const urlObj = new URL(url, window.location.origin);\n\ - const targetDomain = urlObj.hostname;\n\ - \n\ - // If URL has a different domain than current domain, change it\n\ - if (targetDomain !== currentDomain && url.includes("/api/")) {\n\ - console.warn("Redirecting API request to current domain:", url);\n\ - const newUrl = url.replace(targetDomain, currentDomain);\n\ - console.log("New URL:", newUrl);\n\ - return originalFetch(newUrl, options);\n\ + try {\n\ + // Handle both absolute and relative URLs\n\ + const urlObj = new URL(url, window.location.origin);\n\ + const targetDomain = urlObj.hostname;\n\ + \n\ + // If URL has a different domain than current domain, rewrite it\n\ + if (targetDomain !== currentDomain) {\n\ + // Allow external APIs like umami\n\ + if (targetDomain.includes('api-gateway.umami.dev')) {\n\ + return originalFetch(url, options);\n\ + }\n\ + \n\ + console.warn("[Domain Isolation] Redirecting request to current domain:", url);\n\ + const newUrl = url.replace(/https?:\\/\\/[^\\/]+/, window.location.origin);\n\ + console.log("[Domain Isolation] New URL:", newUrl);\n\ + return originalFetch(newUrl, options);\n\ + }\n\ + } catch (e) {\n\ + console.error("[Domain Isolation] Error processing URL:", e);\n\ }\n\ }\n\ - \n\ - // Call the original fetch with unchanged URL\n\ return originalFetch(url, options);\n\ };\n\ \n\ - console.log("API interceptor installed successfully");\n\ + // 2. Intercept XMLHttpRequest\n\ + const originalXHROpen = XMLHttpRequest.prototype.open;\n\ + XMLHttpRequest.prototype.open = function(method, url, ...rest) {\n\ + if (typeof url === "string") {\n\ + try {\n\ + const urlObj = new URL(url, window.location.origin);\n\ + const targetDomain = urlObj.hostname;\n\ + \n\ + if (targetDomain !== currentDomain) {\n\ + // Allow external APIs\n\ + if (targetDomain.includes('api-gateway.umami.dev')) {\n\ + return originalXHROpen.call(this, method, url, ...rest);\n\ + }\n\ + \n\ + console.warn("[Domain Isolation] Redirecting XHR to current domain:", url);\n\ + const newUrl = url.replace(/https?:\\/\\/[^\\/]+/, window.location.origin);\n\ + console.log("[Domain Isolation] New XHR URL:", newUrl);\n\ + return originalXHROpen.call(this, method, newUrl, ...rest);\n\ + }\n\ + } catch (e) {\n\ + console.error("[Domain Isolation] Error processing XHR URL:", e);\n\ + }\n\ + }\n\ + return originalXHROpen.call(this, method, url, ...rest);\n\ + };\n\ + \n\ + // 3. Fix Next.js chunk loading issues\n\ + const originalReactDOMCreateScriptHook = Object.getOwnPropertyDescriptor(HTMLScriptElement.prototype, 'src');\n\ + if (originalReactDOMCreateScriptHook) {\n\ + Object.defineProperty(HTMLScriptElement.prototype, 'src', {\n\ + get: originalReactDOMCreateScriptHook.get,\n\ + set: function(url) {\n\ + if (typeof url === 'string') {\n\ + try {\n\ + const urlObj = new URL(url, window.location.origin);\n\ + const targetDomain = urlObj.hostname;\n\ + \n\ + if (targetDomain !== currentDomain && url.includes('/next/static/chunks/')) {\n\ + const newUrl = url.replace(/https?:\\/\\/[^\\/]+/, window.location.origin);\n\ + console.warn("[Domain Isolation] Redirecting script src to current domain:", url);\n\ + console.log("[Domain Isolation] New script src:", newUrl);\n\ + return originalReactDOMCreateScriptHook.set.call(this, newUrl);\n\ + }\n\ + } catch (e) {\n\ + console.error("[Domain Isolation] Error processing script URL:", e);\n\ + }\n\ + }\n\ + return originalReactDOMCreateScriptHook.set.call(this, url);\n\ + },\n\ + configurable: true\n\ + });\n\ + }\n\ + \n\ + console.log("[Domain Isolation] Complete domain isolation system installed");\n\ })();\n\ EOF\n\ \n\ diff --git a/apps/web/middleware.js b/apps/web/middleware.js new file mode 100644 index 00000000..729b81ae --- /dev/null +++ b/apps/web/middleware.js @@ -0,0 +1,49 @@ +import { NextResponse } from 'next/server'; + +// This middleware runs on every request +export function middleware(request) { + // Get the current hostname from the request headers + const currentHostname = request.headers.get('host'); + + // Get the referrer URL if it exists + const referer = request.headers.get('referer'); + + // If there is a referrer, check if it's from a different domain + if (referer) { + try { + const refererUrl = new URL(referer); + const refererHostname = refererUrl.hostname; + + // If the referrer hostname doesn't match the current hostname + if (refererHostname !== currentHostname) { + console.log(`Cross-domain request detected: ${refererHostname} -> ${currentHostname}`); + + // For path segments that might include another domain + const url = request.nextUrl.clone(); + const path = url.pathname; + + // Check if the path includes another domain name (simple check for static files) + if (path.includes('/next/static/') || path.includes('/api/')) { + // Ensure all paths use the current hostname + // This prevents asset URL problems when different hostnames appear in the path + const localPath = path.replace(/https?:\/\/[^\/]+/, ''); + url.pathname = localPath; + return NextResponse.rewrite(url); + } + } + } catch (e) { + console.error('Error processing referer in middleware:', e); + } + } + + // Continue with the request as normal + return NextResponse.next(); +} + +// Configure which paths this middleware will run on +export const config = { + matcher: [ + // Apply to all paths + '/:path*', + ], +}; \ No newline at end of file diff --git a/apps/web/pages/_document.js b/apps/web/pages/_document.js index c26fcc11..ec01a7b9 100644 --- a/apps/web/pages/_document.js +++ b/apps/web/pages/_document.js @@ -4,10 +4,12 @@ export default function Document() { return ( - {/* Load runtime configuration before any app code */} -