Add validation for health check hostname

Fixes #3677
This commit is contained in:
Owen
2026-09-02 10:42:59 -04:00
parent 8d7e73afa8
commit e0937a3afa
4 changed files with 64 additions and 21 deletions
+1
View File
@@ -2717,6 +2717,7 @@
"healthScheme": "Method", "healthScheme": "Method",
"healthSelectScheme": "Select Method", "healthSelectScheme": "Select Method",
"healthCheckPortInvalid": "Port must be between 1 and 65535", "healthCheckPortInvalid": "Port must be between 1 and 65535",
"healthCheckHostnameInvalid": "Hostname must not contain whitespace",
"healthCheckPath": "Path", "healthCheckPath": "Path",
"healthHostname": "IP / Host", "healthHostname": "IP / Host",
"healthPort": "Port", "healthPort": "Port",
+31 -19
View File
@@ -121,25 +121,37 @@ export async function applyBlueprint({
(hc) => hc.targetId === target.targetId (hc) => hc.targetId === target.targetId
); );
if (["http", "tcp", "udp"].includes(target.mode)) { // The DB writes for all resources have already committed
await addProxyTargets( // by this point, so a push failure for one target (e.g.
site.newt.newtId, // a newt rejecting a malformed health check) must not
[target], // abort pushing the rest, and must not mark the whole
matchingHealthcheck // blueprint as failed when the config was actually
? [matchingHealthcheck] // persisted successfully.
: [], try {
result.proxyResource.mode === "udp" if (["http", "tcp", "udp"].includes(target.mode)) {
? "udp" await addProxyTargets(
: "tcp", site.newt.newtId,
site.newt.version [target],
); matchingHealthcheck
} else if ( ? [matchingHealthcheck]
["ssh", "rdp", "vnc"].includes(target.mode) : [],
) { result.proxyResource.mode === "udp"
await sendBrowserGatewayTargets( ? "udp"
site.newt.newtId, : "tcp",
[target], site.newt.version
site.newt.version );
} else if (
["ssh", "rdp", "vnc"].includes(target.mode)
) {
await sendBrowserGatewayTargets(
site.newt.newtId,
[target],
site.newt.version
);
}
} catch (e) {
logger.error(
`Failed to push target ${target.targetId} to newt on site ${site.sites.siteId}. Error: ${e}`
); );
} }
} }
+27 -1
View File
@@ -29,8 +29,34 @@ export const SiteSchema = z.object({
"docker-socket-enabled": z.boolean().optional().default(true) "docker-socket-enabled": z.boolean().optional().default(true)
}); });
// A malformed hostname (e.g. stray whitespace) is silently accepted here but
// fails to parse as a URL when newt builds the health check request, which
// takes the target out of the routing pool and breaks the resource entirely
// (see #3677). Validate eagerly so blueprints reject it up front instead.
const healthCheckHostnameSchema = z
.string()
.trim()
.min(1)
.refine((val) => !/\s/.test(val), {
message: "Hostname must not contain whitespace"
})
.refine(
(val) => {
if (z.union([z.ipv4(), z.ipv6()]).safeParse(val).success) {
return true;
}
const hostnameRegex =
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
return hostnameRegex.test(val);
},
{
message:
"Hostname must be a valid IP address or hostname (no spaces or invalid characters)"
}
);
export const TargetHealthCheckSchema = z.object({ export const TargetHealthCheckSchema = z.object({
hostname: z.string(), hostname: healthCheckHostnameSchema,
port: z.int().min(1).max(65535), port: z.int().min(1).max(65535),
enabled: z.boolean().optional().default(true), enabled: z.boolean().optional().default(true),
path: z.string().optional().default("/"), path: z.string().optional().default("/"),
+5 -1
View File
@@ -172,7 +172,6 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
.nullable() .nullable()
.optional(), .optional(),
hcScheme: z.string().optional(), hcScheme: z.string().optional(),
hcHostname: z.string(),
hcPort: z hcPort: z
.string() .string()
.min(1, { message: t("healthCheckPortInvalid") }) .min(1, { message: t("healthCheckPortInvalid") })
@@ -184,6 +183,11 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
{ message: t("healthCheckPortInvalid") } { message: t("healthCheckPortInvalid") }
), ),
hcFollowRedirects: z.boolean(), hcFollowRedirects: z.boolean(),
hcHostname: z
.string()
.refine((val) => !/\s/.test(val), {
message: t("healthCheckHostnameInvalid")
}),
hcMode: z.string(), hcMode: z.string(),
hcUnhealthyInterval: z.int().positive().min(5), hcUnhealthyInterval: z.int().positive().min(5),
hcTlsServerName: z.string(), hcTlsServerName: z.string(),