From 066df2c64f7332a331e0b0b89bf7dabf2a871037 Mon Sep 17 00:00:00 2001 From: Yasir Rafique Date: Fri, 18 Jul 2025 10:59:02 +0100 Subject: [PATCH 1/4] Relax monitor URL validation --- client/src/Validation/validation.js | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index b2fe7de71..7459de13d 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -128,26 +128,21 @@ const monitorValidation = joi.object({ .string() .trim() .custom((value, helpers) => { - // Regex from https://gist.github.com/dperini/729294 + const noProtocol = value.replace(/^(https?:\/\/)/, ""); + + if (/^[a-zA-Z0-9-]+(:\d{1,5})?$/.test(noProtocol)) { + return value; + } + + // Accept full domain/IP/URL (as fallback) var urlRegex = new RegExp( "^" + - // protocol identifier (optional) - // short syntax // still required "(?:(?:https?|ftp):\\/\\/)?" + - // user:pass BasicAuth (optional) "(?:" + - // IP address dotted notation octets - // excludes loopback network 0.0.0.0 - // excludes reserved space >= 224.0.0.0 - // excludes network & broadcast addresses - // (first & last IP address of each class) "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + "|" + - // host & domain names, may end with dot - // can be replaced by a shortest alternative - // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ "(?:" + "(?:" + "[a-z0-9\\u00a1-\\uffff]" + @@ -155,21 +150,19 @@ const monitorValidation = joi.object({ ")?" + "[a-z0-9\\u00a1-\\uffff]\\." + ")+" + - // TLD identifier name, may end with dot "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + ")" + - // port number (optional) "(?::\\d{2,5})?" + - // resource path (optional) "(?:[/?#]\\S*)?" + "$", "i" ); - if (!urlRegex.test(value)) { - return helpers.error("string.invalidUrl"); + + if (urlRegex.test(value)) { + return value; } - return value; + return helpers.error("string.invalidUrl"); }) .messages({ "string.empty": "This field is required.", From b823f0f7db0f0cee6bd0c511968883a2027fae92 Mon Sep 17 00:00:00 2001 From: Yasir Rafique Date: Fri, 18 Jul 2025 11:40:20 +0100 Subject: [PATCH 2/4] Relax monitor URL validation for Docker and local hostnames (fixes #2605) --- client/src/Validation/validation.js | 2 +- server/validation/joi.js | 47 ++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 7459de13d..887e4e487 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -130,7 +130,7 @@ const monitorValidation = joi.object({ .custom((value, helpers) => { const noProtocol = value.replace(/^(https?:\/\/)/, ""); - if (/^[a-zA-Z0-9-]+(:\d{1,5})?$/.test(noProtocol)) { + if (/^[a-zA-Z0-9.-]+(:\d{1,5})?$/.test(noProtocol)) { return value; } diff --git a/server/validation/joi.js b/server/validation/joi.js index f555756eb..8549d470a 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -168,7 +168,52 @@ const createMonitorBodyValidation = joi.object({ name: joi.string().required(), description: joi.string().required(), type: joi.string().required(), - url: joi.string().required(), + url: joi + .string() + .required() + .custom((value, helpers) => { + const noProtocol = value.replace(/^(https?:\/\/)/, ""); + + if (/^[a-zA-Z0-9.-]+(:\d{1,5})?$/.test(noProtocol)) { + return value; + } + + // Existing complex URL validation as fallback + var urlRegex = new RegExp( + "^" + + "(?:(?:https?|ftp):\\/\\/)?" + + "(?:" + + "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + + "|" + + "(?:" + + "(?:" + + "[a-z0-9\\u00a1-\\uffff]" + + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + + ")?" + + "[a-z0-9\\u00a1-\\uffff]\\." + + ")+" + + "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + + ")" + + "(?::\\d{2,5})?" + + "(?:[/?#]\\S*)?" + + "$", + "i" + ); + + if (urlRegex.test(value)) { + return value; + } + + return helpers.error("string.invalidUrl"); + }) + .messages({ + "string.empty": "This field is required.", + "string.uri": "The URL you provided is not valid.", + "string.invalidUrl": + "Please enter a valid URL, hostname, or container name (with optional port).", + }), ignoreTlsErrors: joi.boolean().default(false), port: joi.number(), isActive: joi.boolean(), From 8ffd9a5e2a396a10a9ba9b136efaa198c81d63b4 Mon Sep 17 00:00:00 2001 From: Yasir Rafique Date: Fri, 18 Jul 2025 11:58:27 +0100 Subject: [PATCH 3/4] Tighten Docker hostname regex for monitor URL validation --- client/src/Validation/validation.js | 2 +- server/validation/joi.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 887e4e487..7676d65d8 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -130,7 +130,7 @@ const monitorValidation = joi.object({ .custom((value, helpers) => { const noProtocol = value.replace(/^(https?:\/\/)/, ""); - if (/^[a-zA-Z0-9.-]+(:\d{1,5})?$/.test(noProtocol)) { + if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(noProtocol)) { return value; } diff --git a/server/validation/joi.js b/server/validation/joi.js index 8549d470a..3fb652736 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -174,7 +174,7 @@ const createMonitorBodyValidation = joi.object({ .custom((value, helpers) => { const noProtocol = value.replace(/^(https?:\/\/)/, ""); - if (/^[a-zA-Z0-9.-]+(:\d{1,5})?$/.test(noProtocol)) { + if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(noProtocol)) { return value; } From 945cff978bd9c54cec974096f310230667684c72 Mon Sep 17 00:00:00 2001 From: Yasir Rafique Date: Sat, 19 Jul 2025 06:53:01 +0100 Subject: [PATCH 4/4] Use URL.canParse for URL validation; add strict regex fallback (maintainer feedback) --- client/src/Validation/validation.js | 56 ++++++++++++++--------------- server/validation/joi.js | 56 ++++++++++++++--------------- 2 files changed, 52 insertions(+), 60 deletions(-) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 7676d65d8..94575ce99 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -128,40 +128,36 @@ const monitorValidation = joi.object({ .string() .trim() .custom((value, helpers) => { - const noProtocol = value.replace(/^(https?:\/\/)/, ""); + // 1. Standard URLs: must have protocol and pass canParse() + if (/^(https?:\/\/)/.test(value)) { + if ( + typeof URL !== "undefined" && + typeof URL.canParse === "function" && + URL.canParse(value) + ) { + return value; + } + // else, it's a malformed URL with protocol + return helpers.error("string.invalidUrl"); + } - if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(noProtocol)) { - return value; - } - - // Accept full domain/IP/URL (as fallback) - var urlRegex = new RegExp( - "^" + - "(?:(?:https?|ftp):\\/\\/)?" + - "(?:" + - "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + - "|" + - "(?:" + - "(?:" + - "[a-z0-9\\u00a1-\\uffff]" + - "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + - ")?" + - "[a-z0-9\\u00a1-\\uffff]\\." + - ")+" + - "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + - ")" + - "(?::\\d{2,5})?" + - "(?:[/?#]\\S*)?" + - "$", - "i" - ); - - if (urlRegex.test(value)) { + // 2. Docker/internal hostnames (no protocol) + if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(value)) { + if (value.includes("..")) { + return helpers.error("string.invalidUrl"); + } + // Split by dot, check each label + const labels = value.split(":")[0].split("."); + for (const label of labels) { + if (!label) return helpers.error("string.invalidUrl"); + if (label.startsWith("-") || label.endsWith("-")) { + return helpers.error("string.invalidUrl"); + } + } return value; } + // 3. Everything else is invalid return helpers.error("string.invalidUrl"); }) .messages({ diff --git a/server/validation/joi.js b/server/validation/joi.js index 3fb652736..bf69bd06f 100755 --- a/server/validation/joi.js +++ b/server/validation/joi.js @@ -172,40 +172,36 @@ const createMonitorBodyValidation = joi.object({ .string() .required() .custom((value, helpers) => { - const noProtocol = value.replace(/^(https?:\/\/)/, ""); + // 1. Standard URLs: must have protocol and pass canParse() + if (/^(https?:\/\/)/.test(value)) { + if ( + typeof URL !== "undefined" && + typeof URL.canParse === "function" && + URL.canParse(value) + ) { + return value; + } + // else, it's a malformed URL with protocol + return helpers.error("string.invalidUrl"); + } - if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(noProtocol)) { - return value; - } - - // Existing complex URL validation as fallback - var urlRegex = new RegExp( - "^" + - "(?:(?:https?|ftp):\\/\\/)?" + - "(?:" + - "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + - "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + - "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + - "|" + - "(?:" + - "(?:" + - "[a-z0-9\\u00a1-\\uffff]" + - "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + - ")?" + - "[a-z0-9\\u00a1-\\uffff]\\." + - ")+" + - "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + - ")" + - "(?::\\d{2,5})?" + - "(?:[/?#]\\S*)?" + - "$", - "i" - ); - - if (urlRegex.test(value)) { + // 2. Docker/internal hostnames (no protocol) + if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(value)) { + if (value.includes("..")) { + return helpers.error("string.invalidUrl"); + } + // Split by dot, check each label + const labels = value.split(":")[0].split("."); + for (const label of labels) { + if (!label) return helpers.error("string.invalidUrl"); + if (label.startsWith("-") || label.endsWith("-")) { + return helpers.error("string.invalidUrl"); + } + } return value; } + // 3. Everything else is invalid return helpers.error("string.invalidUrl"); }) .messages({