From 976161647ff30dbec0399d533f64b324f175dc95 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 28 Aug 2026 16:43:23 -0600 Subject: [PATCH 1/2] fix: domain validation false negative on servers with multiple IPs Validate against the server's SSH IP plus its detected public egress IP, instead of only the stored SSH address. Fixes #4658 --- .../application/domains/show-domains.tsx | 3 +- apps/dokploy/server/api/routers/domain.ts | 6 +- packages/server/src/services/domain.ts | 58 +++++++++++++++++-- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx index 6a6ad62ae..f4206ee95 100644 --- a/apps/dokploy/components/dashboard/application/domains/show-domains.tsx +++ b/apps/dokploy/components/dashboard/application/domains/show-domains.tsx @@ -191,8 +191,7 @@ export const ShowDomains = ({ id, type }: Props) => { try { const result = await validateDomain({ domain: host, - serverIp: - application?.server?.ipAddress?.toString() || ip?.toString() || "", + serverId: application?.serverId ?? undefined, }); setValidationStates((prev) => ({ diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index 505db50c6..4917b966e 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -7,6 +7,7 @@ import { findPreviewDeploymentById, findServerById, generateTraefikMeDomain, + getServerIpCandidates, getWebServerSettings, manageDomain, removeDomain, @@ -248,10 +249,11 @@ export const domainRouter = createTRPCRouter({ .input( z.object({ domain: z.string(), - serverIp: z.string().optional(), + serverId: z.string().optional(), }), ) .mutation(async ({ input }) => { - return validateDomain(input.domain, input.serverIp); + const expectedIps = await getServerIpCandidates(input.serverId); + return validateDomain(input.domain, expectedIps); }), }); diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts index c651af9fb..a7833341b 100644 --- a/packages/server/src/services/domain.ts +++ b/packages/server/src/services/domain.ts @@ -3,7 +3,9 @@ import { promisify } from "node:util"; import { db } from "@dokploy/server/db"; import { getWebServerSettings } from "@dokploy/server/services/web-server-settings"; import { generateRandomDomain } from "@dokploy/server/templates"; +import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { manageDomain } from "@dokploy/server/utils/traefik/domain"; +import { getPublicIpWithFallback } from "@dokploy/server/wss/utils"; import { TRPCError } from "@trpc/server"; import { eq } from "drizzle-orm"; import type { z } from "zod"; @@ -154,7 +156,7 @@ const resolveDns = promisify(dns.resolve4); export const validateDomain = async ( domain: string, - expectedIp?: string, + expectedIps?: string[], ): Promise<{ isValid: boolean; resolvedIp?: string; @@ -186,13 +188,13 @@ export const validateDomain = async ( }; } - // If we have an expected IP, validate against it - if (expectedIp) { + if (expectedIps && expectedIps.length > 0) { + const isValid = resolvedIps.some((ip) => expectedIps.includes(ip)); return { - isValid: resolvedIps.includes(expectedIp), + isValid, resolvedIp: resolvedIps.join(", "), - error: !resolvedIps.includes(expectedIp) - ? `Domain resolves to ${resolvedIps.join(", ")} but should point to ${expectedIp}` + error: !isValid + ? `Domain resolves to ${resolvedIps.join(", ")} but should point to ${expectedIps.join(" or ")}` : undefined, }; } @@ -210,3 +212,47 @@ export const validateDomain = async ( }; } }; + +export const getServerIpCandidates = async ( + serverId?: string | null, +): Promise => { + const candidates = new Set(); + + if (serverId) { + const server = await findServerById(serverId); + if (server.ipAddress) { + candidates.add(server.ipAddress); + } + + const publicIp = await withTimeout( + execAsyncRemote( + serverId, + "curl -s -m 5 https://ifconfig.me || curl -s -m 5 https://icanhazip.com", + ), + 7000, + ); + const detectedIp = publicIp?.stdout?.trim(); + if (detectedIp) { + candidates.add(detectedIp); + } + } else { + const settings = await getWebServerSettings(); + if (settings?.serverIp) { + candidates.add(settings.serverIp); + } + + const publicIp = await withTimeout(getPublicIpWithFallback(), 7000); + if (publicIp) { + candidates.add(publicIp); + } + } + + return Array.from(candidates); +}; + +const withTimeout = (promise: Promise, ms: number): Promise => { + return Promise.race([ + promise, + new Promise((resolve) => setTimeout(() => resolve(null), ms)), + ]).catch(() => null); +}; From 4b7a6295fc88decc3d032d1bb058bdd4f77cb2a2 Mon Sep 17 00:00:00 2001 From: Mauricio Siu Date: Fri, 28 Aug 2026 16:49:06 -0600 Subject: [PATCH 2/2] fix: verify server ownership before running IP detection validateDomain accepted an arbitrary serverId and used it to look up the server and SSH into it, without checking it belonged to the caller's active organization. A member with domain:read could probe other orgs' servers and get their SSH/detected IPs back in the response. Addresses greptile review on #5214 --- apps/dokploy/server/api/routers/domain.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/server/api/routers/domain.ts b/apps/dokploy/server/api/routers/domain.ts index 4917b966e..10c0b8a8c 100644 --- a/apps/dokploy/server/api/routers/domain.ts +++ b/apps/dokploy/server/api/routers/domain.ts @@ -252,7 +252,17 @@ export const domainRouter = createTRPCRouter({ serverId: z.string().optional(), }), ) - .mutation(async ({ input }) => { + .mutation(async ({ input, ctx }) => { + if (input.serverId) { + const server = await findServerById(input.serverId); + if (server.organizationId !== ctx.session.activeOrganizationId) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You are not authorized to access this server", + }); + } + } + const expectedIps = await getServerIpCandidates(input.serverId); return validateDomain(input.domain, expectedIps); }),