From f3d280f1b0147ec6e4295732f228e01d9cafe265 Mon Sep 17 00:00:00 2001 From: Anurag Ekkati Date: Fri, 2 Jan 2026 14:59:39 -0800 Subject: [PATCH 1/7] fix(monitor): DNS monitor hostname and other monitors URL validations Fixes Issue #6444 Summary: * DNS monitor hostname input will accept wildcard and rejects IP (Valid : *.testdns.co, Invalid : 8.8.8.8) * http, keyword, json-query, websocket, real-browser monitors will not accept wildcard hostnames in URL (Invalid : https://*.testdns.co/status) --- package-lock.json | 10 +++++++++ package.json | 3 ++- src/pages/EditMonitor.vue | 46 ++++++++++++++++++++++++++++++++++++++- src/util-frontend.js | 4 ++-- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab56e410d..5d1e8fe98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ "thirty-two": "~1.0.2", "tldts": "^7.0.19", "tough-cookie": "~4.1.3", + "validator": "^13.15.26", "web-push": "^3.6.7", "ws": "^8.13.0" }, @@ -18742,6 +18743,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/varint": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz", diff --git a/package.json b/package.json index 987b5af52..4d4173217 100644 --- a/package.json +++ b/package.json @@ -122,8 +122,8 @@ "net-snmp": "^3.11.2", "node-cloudflared-tunnel": "~1.0.9", "node-fetch-cache": "^5.1.0", - "nodemailer": "~7.0.12", "node-radius-utils": "~1.2.0", + "nodemailer": "~7.0.12", "nostr-tools": "^2.10.4", "notp": "~2.0.3", "openid-client": "^5.4.2", @@ -149,6 +149,7 @@ "thirty-two": "~1.0.2", "tldts": "^7.0.19", "tough-cookie": "~4.1.3", + "validator": "^13.15.26", "web-push": "^3.6.7", "ws": "^8.13.0" }, diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index f72e68423..d5fb6d1a5 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -326,7 +326,7 @@ v-model="monitor.hostname" type="text" class="form-control" - :pattern="`${monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : ipOrHostnameRegexPattern}`" + :pattern="monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : (monitor.type === 'dns' ? null : ipOrHostnameRegexPattern)" required data-testid="hostname-input" > @@ -1330,6 +1330,8 @@ import { sleep, } from "../util.ts"; import { hostNameRegexPattern, timeDurationFormatter } from "../util-frontend"; +import isFQDN from "validator/lib/isFQDN"; +import isIP from "validator/lib/isIP"; import HiddenInput from "../components/HiddenInput.vue"; import EditMonitorConditions from "../components/EditMonitorConditions.vue"; @@ -2083,6 +2085,48 @@ message HealthCheckResponse { } } + // Validate the DNS Monitor hostname input + if (this.monitor.type === "dns" && this.monitor.hostname) { + let hostname = this.monitor.hostname.trim(); + + if (isIP(hostname)) { + toast.error("Hostname cannot be an IP address"); + return false; + } + + if (!isFQDN(hostname, { + allow_wildcard: true, + require_tld: false, + allow_underscores: true, + allow_trailing_dot: true, + })) { + toast.error("Invalid hostname"); + return false; + } + } + + // Validate URL field input + if ((this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query" || this.monitor.type === "websocket-upgrade" || this.monitor.type === "real-browser") && this.monitor.url) { + try { + const url = new URL(this.monitor.url); + if (url.hostname.includes("*")) { + toast.error("Wildcard hostnames are only supported for DNS monitors."); + return false; + } + if (!isFQDN(url.hostname, { + require_tld: false, + allow_underscores: true, + allow_trailing_dot: true, + }) && !isIP(url.hostname)) { + toast.error("Invalid hostname"); + return false; + } + } catch (err) { + toast.error("Invalid URL"); + return false; + } + } + return true; }, diff --git a/src/util-frontend.js b/src/util-frontend.js index fdb2a8157..66aabd75b 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -109,10 +109,10 @@ export function getDevContainerServerHostname() { } /** - * Regex pattern fr identifying hostnames and IP addresses + * Regex pattern for identifying hostnames and IP addresses * @param {boolean} mqtt whether or not the regex should take into * account the fact that it is an mqtt uri - * @returns {RegExp} The requested regex + * @returns {string} The requested regex string */ export function hostNameRegexPattern(mqtt = false) { // mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect) From 11aef47731afd02f96d8704c8932e3f750e65649 Mon Sep 17 00:00:00 2001 From: Anurag Ekkati Date: Sat, 3 Jan 2026 14:27:36 -0800 Subject: [PATCH 2/7] Update src/pages/EditMonitor.vue Co-authored-by: Frank Elsinga --- src/pages/EditMonitor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index d5fb6d1a5..bd2a57894 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -2090,7 +2090,7 @@ message HealthCheckResponse { let hostname = this.monitor.hostname.trim(); if (isIP(hostname)) { - toast.error("Hostname cannot be an IP address"); + toast.error("DNS hostname cannot be an IP. Did you mean to use the resolver field?"); return false; } From f9694a21d9158a36cfd19ef7b642c56cfefc5b01 Mon Sep 17 00:00:00 2001 From: Anurag Ekkati Date: Sat, 3 Jan 2026 14:28:00 -0800 Subject: [PATCH 3/7] Update src/pages/EditMonitor.vue Co-authored-by: Frank Elsinga --- src/pages/EditMonitor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index bd2a57894..1cf7b8c0e 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -2100,7 +2100,7 @@ message HealthCheckResponse { allow_underscores: true, allow_trailing_dot: true, })) { - toast.error("Invalid hostname"); + toast.error("Invalid hostname. Must be a FQDN, wildcard, underscore, or end with a dot."); return false; } } From 08d8278a755b5b7308865d92e0e70dbcbf65fbde Mon Sep 17 00:00:00 2001 From: Anurag Ekkati Date: Sat, 3 Jan 2026 23:42:03 -0800 Subject: [PATCH 4/7] fix(monitor): Addressing review comments for PR #6577 --- src/lang/en.json | 5 +++++ src/pages/EditMonitor.vue | 39 +++++++++++++++++++++++---------------- src/util-frontend.js | 17 ----------------- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/src/lang/en.json b/src/lang/en.json index c75f38660..2f74267cc 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -276,6 +276,11 @@ "mqttWebsocketPathExplanation": "WebSocket path for MQTT over WebSocket connections (e.g., /mqtt)", "mqttWebsocketPathInvalid": "Please use a valid WebSocket Path format", "mqttHostnameTip": "Please use this format {hostnameFormat}", + "hostnameCannotBeIP": "DNS hostname cannot be an IP. Did you mean to use the resolver field?", + "invalidHostnameOrIP": "Invalid hostname or IP. Hostname must be a valid FQDN. Cannot use wildcard. Can have underscore, or end with a dot.", + "invalidDNSHostname": "Invalid hostname. Hostname must be a valid FQDN. Can be a wildcard, have underscore or end with a dot.", + "wildcardOnlyForDNS": "Wildcard hostnames are only supported for DNS monitors.", + "invalidURL": "Invalid URL", "successKeyword": "Success Keyword", "successKeywordExplanation": "MQTT Keyword that will be considered as success", "recent": "Recent", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 1cf7b8c0e..d9d9167d6 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -326,7 +326,6 @@ v-model="monitor.hostname" type="text" class="form-control" - :pattern="monitor.type === 'mqtt' ? mqttIpOrHostnameRegexPattern : (monitor.type === 'dns' ? null : ipOrHostnameRegexPattern)" required data-testid="hostname-input" > @@ -1329,7 +1328,7 @@ import { MIN_INTERVAL_SECOND, sleep, } from "../util.ts"; -import { hostNameRegexPattern, timeDurationFormatter } from "../util-frontend"; +import { timeDurationFormatter } from "../util-frontend"; import isFQDN from "validator/lib/isFQDN"; import isIP from "validator/lib/isIP"; import HiddenInput from "../components/HiddenInput.vue"; @@ -1419,8 +1418,6 @@ export default { acceptedWebsocketCodeOptions: [], dnsresolvetypeOptions: [], kafkaSaslMechanismOptions: [], - ipOrHostnameRegexPattern: hostNameRegexPattern(), - mqttIpOrHostnameRegexPattern: hostNameRegexPattern(true), gameList: null, connectionStringTemplates: { "sqlserver": "Server=,;Database=;User Id=;Password=;Encrypt=;TrustServerCertificate=;Connection Timeout=", @@ -2085,32 +2082,42 @@ message HealthCheckResponse { } } - // Validate the DNS Monitor hostname input - if (this.monitor.type === "dns" && this.monitor.hostname) { + // Validate hostname field input for various monitors + if (this.monitor.hostname && [ "mqtt", "dns", "port", "ping", "steam", "gamedig", "radius", "tailscale-ping", "smtp", "snmp" ].includes(this.monitor.type)) { let hostname = this.monitor.hostname.trim(); - if (isIP(hostname)) { - toast.error("DNS hostname cannot be an IP. Did you mean to use the resolver field?"); + if (this.monitor.type === "mqtt") { + hostname = hostname.replace(/^(mqtt|ws)s?:\/\//, ""); + } + + if (this.monitor.type === "dns" && isIP(hostname)) { + toast.error(this.$t("hostnameCannotBeIP")); return false; } + // Wildcard is allowed only for DNS if (!isFQDN(hostname, { - allow_wildcard: true, + allow_wildcard: this.monitor.type === "dns", require_tld: false, allow_underscores: true, allow_trailing_dot: true, - })) { - toast.error("Invalid hostname. Must be a FQDN, wildcard, underscore, or end with a dot."); + }) && !isIP(hostname)) { + if(this.monitor.type === "dns") { + toast.error(this.$t("invalidDNSHostname")); + } else { + toast.error(this.$t("invalidHostnameOrIP")); + } return false; } } - // Validate URL field input + // Validate URL field input for various monitors if ((this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query" || this.monitor.type === "websocket-upgrade" || this.monitor.type === "real-browser") && this.monitor.url) { try { const url = new URL(this.monitor.url); - if (url.hostname.includes("*")) { - toast.error("Wildcard hostnames are only supported for DNS monitors."); + // Browser can encode *.hostname.com to %2A.hostname.com + if (url.hostname.includes("*") || url.hostname.includes("%2A")) { + toast.error(this.$t("wildcardOnlyForDNS")); return false; } if (!isFQDN(url.hostname, { @@ -2118,11 +2125,11 @@ message HealthCheckResponse { allow_underscores: true, allow_trailing_dot: true, }) && !isIP(url.hostname)) { - toast.error("Invalid hostname"); + toast.error(this.$t("invalidHostnameOrIP")); return false; } } catch (err) { - toast.error("Invalid URL"); + toast.error(this.$t("invalidURL")); return false; } } diff --git a/src/util-frontend.js b/src/util-frontend.js index 66aabd75b..5912620b5 100644 --- a/src/util-frontend.js +++ b/src/util-frontend.js @@ -108,23 +108,6 @@ export function getDevContainerServerHostname() { return CODESPACE_NAME + "-3001." + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN; } -/** - * Regex pattern for identifying hostnames and IP addresses - * @param {boolean} mqtt whether or not the regex should take into - * account the fact that it is an mqtt uri - * @returns {string} The requested regex string - */ -export function hostNameRegexPattern(mqtt = false) { - // mqtt, mqtts, ws and wss schemes accepted by mqtt.js (https://github.com/mqttjs/MQTT.js/#connect) - const mqttSchemeRegexPattern = "((mqtt|ws)s?:\\/\\/)?"; - // Source: https://digitalfortress.tech/tips/top-15-commonly-used-regex/ - const ipRegexPattern = `((^${mqtt ? mqttSchemeRegexPattern : ""}((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))$)|(^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?$))`; - // Source: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address - const hostNameRegexPattern = `^${mqtt ? mqttSchemeRegexPattern : ""}([a-zA-Z0-9])?(([a-zA-Z0-9_]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9_])\\.)*([A-Za-z0-9_]|[A-Za-z0-9_][A-Za-z0-9\\-_]*[A-Za-z0-9_])(\\.)?$`; - - return `${ipRegexPattern}|${hostNameRegexPattern}`; -} - /** * Get the tag color options * Shared between components From ed8051f9e37a3bb0d87168c8c169ae62c1ca1daf Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 07:45:05 +0000 Subject: [PATCH 5/7] [autofix.ci] apply automated fixes --- src/pages/EditMonitor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index d9d9167d6..08b5d82d4 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -2102,7 +2102,7 @@ message HealthCheckResponse { allow_underscores: true, allow_trailing_dot: true, }) && !isIP(hostname)) { - if(this.monitor.type === "dns") { + if (this.monitor.type === "dns") { toast.error(this.$t("invalidDNSHostname")); } else { toast.error(this.$t("invalidHostnameOrIP")); From 3f0a7e70ece0a6e1cb55306b6276921767346da4 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sun, 4 Jan 2026 18:01:09 +0100 Subject: [PATCH 6/7] Apply suggestions from code review --- src/pages/EditMonitor.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 08b5d82d4..78bf28396 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -2083,7 +2083,7 @@ message HealthCheckResponse { } // Validate hostname field input for various monitors - if (this.monitor.hostname && [ "mqtt", "dns", "port", "ping", "steam", "gamedig", "radius", "tailscale-ping", "smtp", "snmp" ].includes(this.monitor.type)) { + if ([ "mqtt", "dns", "port", "ping", "steam", "gamedig", "radius", "tailscale-ping", "smtp", "snmp" ].includes(this.monitor.type) && this.monitor.hostname) { let hostname = this.monitor.hostname.trim(); if (this.monitor.type === "mqtt") { @@ -2112,7 +2112,7 @@ message HealthCheckResponse { } // Validate URL field input for various monitors - if ((this.monitor.type === "http" || this.monitor.type === "keyword" || this.monitor.type === "json-query" || this.monitor.type === "websocket-upgrade" || this.monitor.type === "real-browser") && this.monitor.url) { + if (["http", "keyword", "json-query", "websocket-upgrade", "real-browser"].includes(this.monitor.type) && this.monitor.url) { try { const url = new URL(this.monitor.url); // Browser can encode *.hostname.com to %2A.hostname.com From f93c3021cfd61d8c5b4920f4c316773ca2b7a1d1 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 4 Jan 2026 17:02:22 +0000 Subject: [PATCH 7/7] [autofix.ci] apply automated fixes --- src/pages/EditMonitor.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 78bf28396..29eeb67f8 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -2112,7 +2112,7 @@ message HealthCheckResponse { } // Validate URL field input for various monitors - if (["http", "keyword", "json-query", "websocket-upgrade", "real-browser"].includes(this.monitor.type) && this.monitor.url) { + if ([ "http", "keyword", "json-query", "websocket-upgrade", "real-browser" ].includes(this.monitor.type) && this.monitor.url) { try { const url = new URL(this.monitor.url); // Browser can encode *.hostname.com to %2A.hostname.com