diff --git a/server/package-lock.json b/server/package-lock.json index 9255f9889..726759967 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -36,6 +36,7 @@ "multer": "^1.4.5-lts.2", "nodemailer": "8.0.5", "ping": "0.4.4", + "re2": "^1.24.1", "sharp": "0.33.5", "ssl-checker": "2.0.10", "swagger-ui-express": "5.0.1", @@ -866,7 +867,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1493,7 +1493,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1538,7 +1537,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1554,6 +1552,29 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1561,6 +1582,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -2625,6 +2647,18 @@ "node": ">=12" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -4214,7 +4248,6 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4375,7 +4408,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4613,7 +4645,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5277,6 +5308,15 @@ "win32" ] }, + "node_modules/abbrev": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", + "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -5305,7 +5345,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5712,7 +5751,6 @@ "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "dev": true, "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6033,7 +6071,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -6913,7 +6950,6 @@ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz", "integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==", "license": "MIT", - "peer": true, "dependencies": { "cssnano-preset-default": "^7.0.9", "lilconfig": "^3.1.3" @@ -7631,7 +7667,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7998,12 +8033,17 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, "node_modules/express": { "version": "4.22.2", "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -8911,7 +8951,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/handlebars": { @@ -9262,6 +9301,19 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/install-artifact-from-github": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.6.0.tgz", + "integrity": "sha512-wKsuzN8fy8QK7iEUqyWTQmvZ1QFGPn1xyl3/1iIIDthDjS7Hn9HoPwHlNakZirWbCsbad0lZMkr6Xfbpe1pUzw==", + "license": "BSD-3-Clause", + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -9692,7 +9744,6 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -11199,6 +11250,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mjml": { "version": "5.0.0-alpha.6", "resolved": "https://registry.npmjs.org/mjml/-/mjml-5.0.0-alpha.6.tgz", @@ -11952,8 +12015,7 @@ "version": "2.27.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.27.0.tgz", "integrity": "sha512-hC+0LidcL3XE4rp1C4H54KujgXKzbfyTngZTwBByQxsOxCEKZT0MPQ4hOKUH2jU1OYstqdDH4onyHPDzcV0XdQ==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/nano-spawn": { "version": "2.0.0", @@ -12099,6 +12161,54 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-gyp": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.4.0.tgz", + "integrity": "sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-4.0.0.tgz", + "integrity": "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-6.0.1.tgz", + "integrity": "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==", + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -12173,6 +12283,21 @@ "node": ">=4" } }, + "node_modules/nopt": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz", + "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==", + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -12709,7 +12834,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -13367,6 +13491,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/proc-log": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", + "integrity": "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -13579,6 +13712,24 @@ "node": ">= 0.8" } }, + "node_modules/re2": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.24.1.tgz", + "integrity": "sha512-uRl9cLDKuobJQp+6lVz7E3AyVszubUJ0fqAMWout4ocUWTIFvdHgpqLwwMh/vuNGGGJGh2p2mJZJIQr9am9M/A==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "install-artifact-from-github": "^1.6.0", + "nan": "^2.27.0", + "node-gyp": "^12.3.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/uhop" + } + }, "node_modules/react-is-18": { "name": "react-is", "version": "18.3.1", @@ -14639,6 +14790,22 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/tar": { + "version": "7.5.19", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz", + "integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -14667,6 +14834,24 @@ "node": ">=6" } }, + "node_modules/tar/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/teex": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", @@ -14697,7 +14882,6 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -14714,7 +14898,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -14732,9 +14915,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -14903,7 +15084,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15067,7 +15247,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15121,9 +15300,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "6.24.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", - "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", "license": "MIT", "engines": { "node": ">=18.17" @@ -15934,7 +16113,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/server/package.json b/server/package.json index f44dd7031..7c59d2b94 100755 --- a/server/package.json +++ b/server/package.json @@ -58,6 +58,7 @@ "multer": "^1.4.5-lts.2", "nodemailer": "8.0.5", "ping": "0.4.4", + "re2": "^1.24.1", "sharp": "0.33.5", "ssl-checker": "2.0.10", "swagger-ui-express": "5.0.1", diff --git a/server/src/api/validation/monitorValidation.ts b/server/src/api/validation/monitorValidation.ts index 6dea5b856..e8bae9261 100644 --- a/server/src/api/validation/monitorValidation.ts +++ b/server/src/api/validation/monitorValidation.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import RE2 from "re2"; import { booleanCoercion, dnsHostnameRegex, dnsServerValidation } from "./shared.js"; import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js"; import { @@ -71,6 +72,20 @@ const refineStrategyType = (body: { type?: string; strategy?: string }, ctx: z.R } }; +// The regex is executed at check time with RE2, reject patterns RE2 won't accept +const refineRegexPattern = (body: { matchMethod?: string; expectedValue?: string }, ctx: z.RefinementCtx) => { + if (body.matchMethod !== "regex" || !body.expectedValue) return; + try { + new RE2(body.expectedValue); + } catch { + ctx.addIssue({ + code: "custom", + path: ["expectedValue"], + message: "Invalid regex pattern. Backreferences and lookahead/lookbehind are not supported.", + }); + } +}; + const refineHeadMatching = (body: { method?: string; useAdvancedMatching?: boolean; jsonPath?: string }, ctx: z.RefinementCtx) => { if (body.method === "HEAD" && (body.useAdvancedMatching === true || (body.jsonPath ?? "") !== "")) { ctx.addIssue({ @@ -120,7 +135,8 @@ export const createMonitorBodyValidation = z }) .superRefine(refineDnsHostname) .superRefine(refineStrategyType) - .superRefine(refineHeadMatching); + .superRefine(refineHeadMatching) + .superRefine(refineRegexPattern); export const editMonitorBodyValidation = z .object({ @@ -159,7 +175,8 @@ export const editMonitorBodyValidation = z }) .superRefine(refineDnsHostname) .superRefine(refineStrategyType) - .superRefine(refineHeadMatching); + .superRefine(refineHeadMatching) + .superRefine(refineRegexPattern); export const pauseMonitorParamValidation = z.object({ monitorId: z.string().min(1, "Monitor ID is required"), @@ -232,7 +249,8 @@ const importedMonitorSchema = z }) .superRefine(refineDnsHostname) .superRefine(refineStrategyType) - .superRefine(refineHeadMatching); + .superRefine(refineHeadMatching) + .superRefine(refineRegexPattern); export const importMonitorsBodyValidation = z.object({ monitors: z.array(importedMonitorSchema).min(1, "At least one monitor is required"), diff --git a/server/src/service/network/AdvancedMatcher.ts b/server/src/service/network/AdvancedMatcher.ts index 8b25c53e8..8e77ef3ec 100644 --- a/server/src/service/network/AdvancedMatcher.ts +++ b/server/src/service/network/AdvancedMatcher.ts @@ -1,5 +1,6 @@ import { Monitor } from "@/domain/monitors/monitor.types.js"; import jmespath from "jmespath"; +import RE2 from "re2"; type JmesPath = typeof jmespath; export interface IAdvancedMatcher { @@ -9,13 +10,6 @@ export interface IAdvancedMatcher { export class AdvancedMatcher implements IAdvancedMatcher { constructor(private jmespath: JmesPath) {} - private compare(actual: unknown, expected: string, method?: string): boolean { - if (method === "equal") return String(actual) === expected; - if (method === "include") return String(actual).includes(expected); - if (method === "regex") return new RegExp(expected).test(String(actual)); - return String(actual) === expected; // Default - } - validate(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: T | undefined } { const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor; if (!useAdvancedMatching) return { ok: true, message: "Success" }; @@ -30,19 +24,38 @@ export class AdvancedMatcher implements IAdvancedMatcher { } } - if (expectedValue) { - const ok = this.compare(dataToValidate, expectedValue, matchMethod); + if (!expectedValue) { + const isFalsy = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null; return { - ok, - message: ok ? "Success" : "Expected value did not match", + ok: !isFalsy, + message: !isFalsy ? "Success" : "Extracted value is falsy", extracted: dataToValidate, }; } - const isFalsy = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null; + let actual: string; + try { + actual = String(dataToValidate); + } catch { + return { ok: false, message: "Error evaluating response value", extracted: dataToValidate }; + } + + let ok: boolean; + if (matchMethod === "include") { + ok = actual.includes(expectedValue); + } else if (matchMethod === "regex") { + try { + ok = new RE2(expectedValue).test(actual); + } catch { + return { ok: false, message: "Invalid regex pattern", extracted: dataToValidate }; + } + } else { + ok = actual === expectedValue; + } + return { - ok: !isFalsy, - message: !isFalsy ? "Success" : "Extracted value is falsy", + ok, + message: ok ? "Success" : "Expected value did not match", extracted: dataToValidate, }; } diff --git a/server/test/unit/providers/network/advancedMatcher.test.ts b/server/test/unit/providers/network/advancedMatcher.test.ts index f94e9bb53..2cfb3a7f0 100644 --- a/server/test/unit/providers/network/advancedMatcher.test.ts +++ b/server/test/unit/providers/network/advancedMatcher.test.ts @@ -119,6 +119,34 @@ describe("AdvancedMatcher", () => { expect(result.ok).toBe(false); }); + it("returns a clear error when the regex pattern is rejected by RE2", () => { + const matcher = new AdvancedMatcher(createMockJmespath() as any); + const result = matcher.validate( + { code: "200" }, + // Backreferences are unsupported by RE2 and previously ReDoS-prone; RE2 throws on construction. + makeMonitor({ useAdvancedMatching: true, jsonPath: "code", matchMethod: "regex", expectedValue: "(a+)\\1" }) + ); + + expect(result.ok).toBe(false); + expect(result.message).toBe("Invalid regex pattern"); + expect(result.extracted).toBe("200"); + }); + + it("returns a distinct error when the extracted value cannot be coerced to a string", () => { + // An object with a throwing toString/valueOf (like Object.create(null)) makes String() throw. + const unstringifiable = { toString: () => { throw new Error("nope"); } }; + const jmespath = { search: jest.fn().mockReturnValue(unstringifiable) }; + const matcher = new AdvancedMatcher(jmespath as any); + + const result = matcher.validate( + {}, + makeMonitor({ useAdvancedMatching: true, jsonPath: "field", matchMethod: "equal", expectedValue: "x" }) + ); + + expect(result.ok).toBe(false); + expect(result.message).toBe("Error evaluating response value"); + }); + it("defaults to 'equal' comparison when matchMethod is undefined", () => { const matcher = new AdvancedMatcher(createMockJmespath() as any); const result = matcher.validate({ val: "exact" }, makeMonitor({ useAdvancedMatching: true, jsonPath: "val", expectedValue: "exact" })); diff --git a/server/test/unit/validation/monitorValidation.test.ts b/server/test/unit/validation/monitorValidation.test.ts index d7a68b00b..d6998367a 100644 --- a/server/test/unit/validation/monitorValidation.test.ts +++ b/server/test/unit/validation/monitorValidation.test.ts @@ -278,6 +278,53 @@ describe("monitorValidation — HEAD method gating", () => { }); }); +describe("monitorValidation — regex pattern gating", () => { + const baseRegex = { + name: "regex check", + type: "http" as const, + url: "https://example.com", + useAdvancedMatching: true, + matchMethod: "regex" as const, + }; + + describe("createMonitorBodyValidation", () => { + it("accepts an RE2-supported regex pattern", () => { + const parsed = createMonitorBodyValidation.parse({ ...baseRegex, expectedValue: "^2\\d{2}$" }); + expect(parsed.expectedValue).toBe("^2\\d{2}$"); + }); + + it("rejects a pattern using a backreference", () => { + expect(() => createMonitorBodyValidation.parse({ ...baseRegex, expectedValue: "(a+)\\1" })).toThrow(); + }); + + it("rejects a pattern using lookahead", () => { + expect(() => createMonitorBodyValidation.parse({ ...baseRegex, expectedValue: "foo(?=bar)" })).toThrow(); + }); + + it("ignores an invalid regex when matchMethod is not 'regex'", () => { + const parsed = createMonitorBodyValidation.parse({ ...baseRegex, matchMethod: "include", expectedValue: "(a+)\\1" }); + expect(parsed.expectedValue).toBe("(a+)\\1"); + }); + + it("ignores an empty expectedValue", () => { + const parsed = createMonitorBodyValidation.parse({ ...baseRegex, expectedValue: "" }); + expect(parsed.matchMethod).toBe("regex"); + }); + }); + + describe("editMonitorBodyValidation", () => { + it("rejects a pattern using a backreference", () => { + expect(() => editMonitorBodyValidation.parse({ matchMethod: "regex", expectedValue: "(a+)\\1" })).toThrow(); + }); + }); + + describe("importMonitorsBodyValidation", () => { + it("rejects a pattern using a backreference on imported monitors", () => { + expect(() => importMonitorsBodyValidation.parse({ monitors: [{ ...baseRegex, expectedValue: "(a+)\\1" }] })).toThrow(); + }); + }); +}); + describe("monitorValidation — customUpCodes", () => { const baseHttpBody = { name: "HTTP check",