diff --git a/package-lock.json b/package-lock.json index 8183f9f5..f00ff857 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "azure-iot-explorer", - "version": "0.15.15", + "version": "0.15.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "azure-iot-explorer", - "version": "0.15.15", + "version": "0.15.16", "license": "MIT", "dependencies": { "@azure/core-amqp": "^3.2.0", @@ -1064,9 +1064,9 @@ } }, "node_modules/@develar/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1126,9 +1126,9 @@ } }, "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1576,9 +1576,9 @@ } }, "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -1614,13 +1614,13 @@ } }, "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -1986,29 +1986,6 @@ "@hapi/hoek": "^11.0.2" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -4165,9 +4142,9 @@ "optional": true }, "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "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", "optional": true, @@ -4667,16 +4644,6 @@ "node": ">= 10" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/async-lock": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@types/async-lock/-/async-lock-1.4.2.tgz", @@ -5603,9 +5570,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "version": "0.8.12", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", + "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", "dev": true, "license": "MIT", "engines": { @@ -5768,9 +5735,9 @@ "license": "MIT" }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -6196,9 +6163,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/asn1js": { @@ -6310,15 +6277,15 @@ } }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "node_modules/azure-iot-common": { @@ -6696,9 +6663,9 @@ } }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "license": "MIT" }, "node_modules/body-parser": { @@ -6768,9 +6735,9 @@ "optional": true }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -7217,9 +7184,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -7255,13 +7222,13 @@ "license": "ISC" }, "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -8133,9 +8100,9 @@ } }, "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/create-hash": { @@ -9011,9 +8978,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/dir-compare": { @@ -9028,9 +8995,9 @@ } }, "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -9143,9 +9110,9 @@ } }, "node_modules/dmg-license/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "optional": true, @@ -9850,9 +9817,9 @@ } }, "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/emittery": { @@ -10749,9 +10716,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -10759,9 +10726,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -10852,9 +10819,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -11231,9 +11198,9 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -11362,9 +11329,9 @@ "license": "MIT" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11954,9 +11921,9 @@ "license": "ISC" }, "node_modules/immutable": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", - "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", "license": "MIT" }, "node_modules/import-local": { @@ -15326,15 +15293,15 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", "license": "MIT" }, "node_modules/lodash.escape": { @@ -15837,9 +15804,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/mime": { @@ -15930,21 +15897,44 @@ "license": "MIT" }, "node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -16337,9 +16327,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" @@ -16507,9 +16497,9 @@ } }, "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -17278,9 +17268,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, "node_modules/pbkdf2": { @@ -17337,9 +17327,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -18231,11 +18221,14 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/psl": { "version": "1.15.0", @@ -18272,9 +18265,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "license": "MIT" }, "node_modules/pump": { @@ -18335,9 +18328,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -18545,9 +18538,9 @@ } }, "node_modules/react-jsonschema-form/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -19464,9 +19457,9 @@ } }, "node_modules/sass/node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", "dev": true, "license": "MIT" }, @@ -19485,9 +19478,9 @@ } }, "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -20660,9 +20653,9 @@ } }, "node_modules/style-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -20793,18 +20786,18 @@ } }, "node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz", + "integrity": "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", + "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": { @@ -20919,9 +20912,9 @@ } }, "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -21039,16 +21032,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -21096,9 +21088,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -21213,9 +21205,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "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", "engines": { @@ -21746,9 +21738,9 @@ } }, "node_modules/tslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -23204,9 +23196,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, "license": "ISC", "engines": { diff --git a/package.json b/package.json index 5dab15bb..ac6c13d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "azure-iot-explorer", - "version": "0.15.15", + "version": "0.15.16", "description": "This project welcomes contributions and suggestions. Most contributions require you to agree to a\r Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\r the rights to use your contribution. For details, visit https://cla.microsoft.com.", "main": "host/electron.js", "build": { diff --git a/public/handlers/eventHubHandler.spec.ts b/public/handlers/eventHubHandler.spec.ts new file mode 100644 index 00000000..c1f28cd6 --- /dev/null +++ b/public/handlers/eventHubHandler.spec.ts @@ -0,0 +1,279 @@ +/*********************************************************** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License + **********************************************************/ +import 'jest'; + +// Mock electron +jest.mock('electron', () => ({ + BrowserWindow: jest.fn(), + ipcMain: { handle: jest.fn() } +})); + +// Mock @azure/event-hubs +const mockSubscribe = jest.fn().mockReturnValue({ close: jest.fn() }); +const mockClose = jest.fn(); +const MockEventHubConsumerClient = jest.fn().mockImplementation(() => ({ + subscribe: mockSubscribe, + close: mockClose +})); +jest.mock('@azure/event-hubs', () => ({ + EventHubConsumerClient: MockEventHubConsumerClient, + earliestEventPosition: {} +})); + +// Mock rhea-promise Connection +const mockConnectionOpen = jest.fn().mockResolvedValue(undefined); +const mockConnectionClose = jest.fn().mockResolvedValue(undefined); +const MockConnection = jest.fn().mockImplementation(() => ({ + open: mockConnectionOpen, + close: mockConnectionClose, + createReceiver: jest.fn().mockResolvedValue({ + on: jest.fn((event: string, handler: (context: any) => void) => { + // Simulate an AMQP redirect error (the normal flow for IoT Hub → EventHub conversion) + if (event === 'receiver_error') { + setTimeout(() => { + handler({ + receiver: { + error: { + condition: 'amqp:link:redirect', + info: { + hostname: 'test-redirect.servicebus.windows.net', + address: 'amqps://test-redirect.servicebus.windows.net:5671/test-hub/$management' + } + } + } + }); + }, 0); + } + }) + }) +})); +jest.mock('rhea-promise', () => ({ + Connection: MockConnection, + ReceiverEvents: { receiverError: 'receiver_error' }, + parseConnectionString: jest.fn((cs: string) => { + const obj: any = {}; + cs.split(';').forEach((segment: string) => { + const idx = segment.indexOf('='); + if (idx > 0) { + obj[segment.substring(0, idx)] = segment.substring(idx + 1); + } + }); + return obj; + }), + isAmqpError: jest.fn().mockReturnValue(true) +})); + +jest.mock('@azure/core-amqp', () => ({ + ErrorNameConditionMapper: { LinkRedirectError: 'amqp:link:redirect' } +})); + +import { handleStartEventHubMonitoring } from './eventHubHandler'; + +describe('eventHubHandler hostname validation', () => { + const mockEvent = {} as Electron.IpcMainInvokeEvent; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('custom EventHub connection string (Path 1)', () => { + it('rejects attacker-controlled hostname', async () => { + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + customEventHubConnectionString: 'Endpoint=sb://attacker-controlled-host.com;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==;EntityPath=test' + })).rejects.toThrow('Invalid EventHub hostname'); + + expect(MockEventHubConsumerClient).not.toHaveBeenCalled(); + }); + + it('rejects IP address hostname', async () => { + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + customEventHubConnectionString: 'Endpoint=sb://169.254.169.254;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('Invalid EventHub hostname'); + + expect(MockEventHubConsumerClient).not.toHaveBeenCalled(); + }); + + it('rejects localhost hostname', async () => { + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + customEventHubConnectionString: 'Endpoint=sb://localhost;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('Invalid EventHub hostname'); + + expect(MockEventHubConsumerClient).not.toHaveBeenCalled(); + }); + + it('accepts valid Event Hubs hostname', async () => { + await handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + customEventHubConnectionString: 'Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==;EntityPath=myhub' + }); + + expect(MockEventHubConsumerClient).toHaveBeenCalledWith( + '$Default', + 'Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==;EntityPath=myhub' + ); + }); + + it('accepts valid Private Link Event Hubs hostname', async () => { + await handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + customEventHubConnectionString: 'Endpoint=sb://mynamespace.privatelink.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==;EntityPath=myhub' + }); + + expect(MockEventHubConsumerClient).toHaveBeenCalled(); + }); + + it('rejects connection string without Endpoint', async () => { + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + customEventHubConnectionString: 'SharedAccessKeyName=test;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('unable to extract'); + + expect(MockEventHubConsumerClient).not.toHaveBeenCalled(); + }); + }); + + describe('IoT Hub connection string (Path 2)', () => { + it('rejects attacker-controlled IoT Hub hostname', async () => { + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=evil.com;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('Invalid IoT Hub hostname'); + + expect(MockConnection).not.toHaveBeenCalled(); + }); + + it('rejects IP address as IoT Hub hostname', async () => { + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=192.168.1.1;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('Invalid IoT Hub hostname'); + + expect(MockConnection).not.toHaveBeenCalled(); + }); + + it('accepts valid IoT Hub hostname and creates AMQP connection', async () => { + await handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=myhub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdA==' + }); + + expect(MockConnection).toHaveBeenCalledWith( + expect.objectContaining({ + host: 'myhub.azure-devices.net', + hostname: 'myhub.azure-devices.net', + port: 5671 + }) + ); + expect(mockConnectionOpen).toHaveBeenCalled(); + }); + + it('accepts valid Private Link IoT Hub hostname', async () => { + await handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=myhub.privatelink.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdA==' + }); + + expect(MockConnection).toHaveBeenCalledWith( + expect.objectContaining({ + host: 'myhub.privatelink.azure-devices.net', + hostname: 'myhub.privatelink.azure-devices.net' + }) + ); + }); + }); + + describe('AMQP redirect validation (Path 3 — redirect hostname)', () => { + function mockConnectionWithRedirect(redirectHostname: string) { + MockConnection.mockImplementationOnce(() => ({ + open: jest.fn().mockResolvedValue(undefined), + close: jest.fn().mockResolvedValue(undefined), + createReceiver: jest.fn().mockResolvedValue({ + on: jest.fn((event: string, handler: (context: any) => void) => { + if (event === 'receiver_error') { + setTimeout(() => { + handler({ + receiver: { + error: { + condition: 'amqp:link:redirect', + info: { + hostname: redirectHostname, + address: `amqps://${redirectHostname}:5671/test-hub/$management` + } + } + } + }); + }, 0); + } + }) + }) + })); + } + + it('rejects attacker-controlled AMQP redirect hostname', async () => { + mockConnectionWithRedirect('attacker.com'); + + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=myhub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('Invalid EventHub redirect hostname'); + }); + + it('rejects cloud metadata endpoint in AMQP redirect', async () => { + mockConnectionWithRedirect('169.254.169.254'); + + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=myhub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('Invalid EventHub redirect hostname'); + }); + + it('rejects private IP in AMQP redirect', async () => { + mockConnectionWithRedirect('192.168.1.1'); + + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=myhub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('Invalid EventHub redirect hostname'); + }); + + it('rejects domain-spoofing in AMQP redirect', async () => { + mockConnectionWithRedirect('evil.servicebus.windows.net.attacker.com'); + + await expect(handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=myhub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdA==' + })).rejects.toThrow('Invalid EventHub redirect hostname'); + }); + + it('accepts valid servicebus redirect hostname', async () => { + mockConnectionWithRedirect('test-redirect.servicebus.windows.net'); + + await handleStartEventHubMonitoring(mockEvent, { + deviceId: 'device1', + consumerGroup: '$Default', + hubConnectionString: 'HostName=myhub.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=dGVzdA==' + }); + + expect(MockEventHubConsumerClient).toHaveBeenCalled(); + }); + }); +}); diff --git a/public/handlers/eventHubHandler.ts b/public/handlers/eventHubHandler.ts index f70339b5..9064bc54 100644 --- a/public/handlers/eventHubHandler.ts +++ b/public/handlers/eventHubHandler.ts @@ -10,6 +10,11 @@ import { ErrorNameConditionMapper as AMQPError } from '@azure/core-amqp'; import { EventHubConsumerClient, Subscription, ReceivedEventData, earliestEventPosition } from '@azure/event-hubs'; import { BrowserWindow } from 'electron'; import { MESSAGE_CHANNELS } from '../constants'; +import { + validateAzureIoTHostname, + validateEventHubHostname, + extractEventHubHostname +} from './urlValidator'; export interface Message { body: any; // tslint:disable-line:no-any @@ -68,6 +73,10 @@ export const handleStopEventHubMonitoring = async (): Promise => { */ const initializeEventHubClient = async (params: StartEventHubMonitoringRequest): Promise => { if (params.customEventHubConnectionString) { + const eventHubHost = extractEventHubHostname(params.customEventHubConnectionString); + if (!validateEventHubHostname(eventHubHost)) { + throw new Error('Invalid EventHub hostname: must be a valid Azure Event Hubs endpoint (*.servicebus.windows.net)'); + } client = new EventHubConsumerClient(params.consumerGroup, params.customEventHubConnectionString); } else { client = new EventHubConsumerClient( @@ -194,6 +203,11 @@ export async function convertIotHubToEventHubsConnectionString(connectionString: throw new Error('Invalid IotHub connection string.'); } + // Validate hostname to prevent SSRF + if (!validateAzureIoTHostname(HostName)) { + throw new Error('Invalid IoT Hub hostname: must be a valid Azure IoT Hub endpoint (*.azure-devices.net)'); + } + // Extract the IotHub name from the hostname. const [iotHubName] = HostName.split('.'); @@ -236,6 +250,8 @@ export async function convertIotHubToEventHubsConnectionString(connectionString: const regexResults = regex.exec(iotAddress); if (!hostname || !regexResults) { reject(error); + } else if (!validateEventHubHostname(hostname)) { + reject(new Error('Invalid EventHub redirect hostname: must be a valid Azure Event Hubs endpoint (*.servicebus.windows.net)')); } else { const eventHubName = regexResults[1]; resolve( diff --git a/public/handlers/urlValidator.spec.ts b/public/handlers/urlValidator.spec.ts new file mode 100644 index 00000000..f924c688 --- /dev/null +++ b/public/handlers/urlValidator.spec.ts @@ -0,0 +1,282 @@ +/*********************************************************** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License + **********************************************************/ +import 'jest'; +import { + validateAzureIoTHostname, + validateEventHubHostname, + extractEventHubHostname +} from './urlValidator'; + +describe('validateAzureIoTHostname', () => { + describe('valid hostnames', () => { + it('accepts standard IoT Hub hostname', () => { + expect(validateAzureIoTHostname('myhub.azure-devices.net')).toBe(true); + }); + + it('accepts hostname with hyphens', () => { + expect(validateAzureIoTHostname('my-iot-hub.azure-devices.net')).toBe(true); + }); + + it('accepts hostname with numbers', () => { + expect(validateAzureIoTHostname('hub123.azure-devices.net')).toBe(true); + }); + + it('accepts uppercase (normalized to lowercase)', () => { + expect(validateAzureIoTHostname('MyHub.Azure-Devices.Net')).toBe(true); + }); + + it('accepts Private Link hostname', () => { + expect(validateAzureIoTHostname('myhub.privatelink.azure-devices.net')).toBe(true); + }); + + it('accepts Private Link hostname with hyphens', () => { + expect(validateAzureIoTHostname('my-hub-01.privatelink.azure-devices.net')).toBe(true); + }); + }); + + describe('invalid hostnames — attacker-controlled', () => { + it('rejects attacker domain', () => { + expect(validateAzureIoTHostname('evil.com')).toBe(false); + }); + + it('rejects domain that does not end with azure-devices.net', () => { + expect(validateAzureIoTHostname('myhub.not-azure-devices.net')).toBe(false); + }); + + it('rejects domain with extra subdomain (spoofing)', () => { + expect(validateAzureIoTHostname('evil.myhub.azure-devices.net')).toBe(false); + }); + + it('rejects hostname ending with azure-devices.net but with wrong subdomain pattern', () => { + expect(validateAzureIoTHostname('myhub.fakelink.azure-devices.net')).toBe(false); + }); + + it('rejects attacker domain appended after valid suffix', () => { + expect(validateAzureIoTHostname('myhub.azure-devices.net.evil.com')).toBe(false); + }); + }); + + describe('invalid hostnames — IP addresses', () => { + it('rejects loopback IPv4', () => { + expect(validateAzureIoTHostname('127.0.0.1')).toBe(false); + }); + + it('rejects private IP (RFC 1918)', () => { + expect(validateAzureIoTHostname('192.168.1.1')).toBe(false); + expect(validateAzureIoTHostname('10.0.0.1')).toBe(false); + }); + + it('rejects cloud metadata endpoint', () => { + expect(validateAzureIoTHostname('169.254.169.254')).toBe(false); + }); + }); + + describe('invalid hostnames — injection attacks', () => { + it('rejects path injection with slash', () => { + expect(validateAzureIoTHostname('myhub.azure-devices.net/evil')).toBe(false); + }); + + it('rejects path injection with backslash', () => { + expect(validateAzureIoTHostname('myhub.azure-devices.net\\evil')).toBe(false); + }); + + it('rejects encoded path injection', () => { + expect(validateAzureIoTHostname('myhub.azure-devices.net%2Fevil')).toBe(false); + }); + + it('rejects credential injection with @', () => { + expect(validateAzureIoTHostname('user@myhub.azure-devices.net')).toBe(false); + }); + + it('rejects port injection with :', () => { + expect(validateAzureIoTHostname('myhub.azure-devices.net:5671')).toBe(false); + }); + }); + + describe('invalid hostnames — edge cases', () => { + it('rejects empty string', () => { + expect(validateAzureIoTHostname('')).toBe(false); + }); + + it('rejects null', () => { + expect(validateAzureIoTHostname(null as any)).toBe(false); + }); + + it('rejects undefined', () => { + expect(validateAzureIoTHostname(undefined as any)).toBe(false); + }); + + it('rejects bare suffix with no hub name', () => { + expect(validateAzureIoTHostname('.azure-devices.net')).toBe(false); + }); + + it('rejects hub name starting with hyphen', () => { + expect(validateAzureIoTHostname('-myhub.azure-devices.net')).toBe(false); + }); + + it('rejects hub name ending with hyphen', () => { + expect(validateAzureIoTHostname('myhub-.azure-devices.net')).toBe(false); + }); + + it('rejects just the suffix without subdomain', () => { + expect(validateAzureIoTHostname('azure-devices.net')).toBe(false); + }); + }); +}); + +describe('validateEventHubHostname', () => { + describe('valid hostnames', () => { + it('accepts standard Event Hubs hostname', () => { + expect(validateEventHubHostname('mynamespace.servicebus.windows.net')).toBe(true); + }); + + it('accepts hostname with hyphens', () => { + expect(validateEventHubHostname('my-namespace.servicebus.windows.net')).toBe(true); + }); + + it('accepts hostname with numbers', () => { + expect(validateEventHubHostname('ns123.servicebus.windows.net')).toBe(true); + }); + + it('accepts uppercase (normalized to lowercase)', () => { + expect(validateEventHubHostname('MyNamespace.ServiceBus.Windows.Net')).toBe(true); + }); + + it('accepts Private Link hostname', () => { + expect(validateEventHubHostname('mynamespace.privatelink.servicebus.windows.net')).toBe(true); + }); + + it('accepts Private Link hostname with hyphens', () => { + expect(validateEventHubHostname('my-ns-01.privatelink.servicebus.windows.net')).toBe(true); + }); + }); + + describe('invalid hostnames — attacker-controlled', () => { + it('rejects attacker domain', () => { + expect(validateEventHubHostname('evil.com')).toBe(false); + }); + + it('rejects attacker-controlled-host.com', () => { + expect(validateEventHubHostname('attacker-controlled-host.com')).toBe(false); + }); + + it('rejects domain that does not end with servicebus.windows.net', () => { + expect(validateEventHubHostname('mynamespace.not-servicebus.windows.net')).toBe(false); + }); + + it('rejects domain with extra subdomain (spoofing)', () => { + expect(validateEventHubHostname('evil.mynamespace.servicebus.windows.net')).toBe(false); + }); + + it('rejects hostname with wrong privatelink position', () => { + expect(validateEventHubHostname('mynamespace.fakelink.servicebus.windows.net')).toBe(false); + }); + + it('rejects attacker domain appended after valid suffix', () => { + expect(validateEventHubHostname('mynamespace.servicebus.windows.net.evil.com')).toBe(false); + }); + + it('rejects just the suffix without namespace', () => { + expect(validateEventHubHostname('servicebus.windows.net')).toBe(false); + }); + }); + + describe('invalid hostnames — IP addresses', () => { + it('rejects loopback IPv4', () => { + expect(validateEventHubHostname('127.0.0.1')).toBe(false); + }); + + it('rejects private IP (RFC 1918)', () => { + expect(validateEventHubHostname('192.168.1.1')).toBe(false); + expect(validateEventHubHostname('10.0.0.1')).toBe(false); + }); + + it('rejects cloud metadata endpoint', () => { + expect(validateEventHubHostname('169.254.169.254')).toBe(false); + }); + }); + + describe('invalid hostnames — injection attacks', () => { + it('rejects path injection with slash', () => { + expect(validateEventHubHostname('mynamespace.servicebus.windows.net/evil')).toBe(false); + }); + + it('rejects path injection with backslash', () => { + expect(validateEventHubHostname('mynamespace.servicebus.windows.net\\evil')).toBe(false); + }); + + it('rejects encoded path injection', () => { + expect(validateEventHubHostname('mynamespace.servicebus.windows.net%2Fevil')).toBe(false); + }); + + it('rejects credential injection with @', () => { + expect(validateEventHubHostname('user@mynamespace.servicebus.windows.net')).toBe(false); + }); + + it('rejects port injection with :', () => { + expect(validateEventHubHostname('mynamespace.servicebus.windows.net:5671')).toBe(false); + }); + }); + + describe('invalid hostnames — edge cases', () => { + it('rejects empty string', () => { + expect(validateEventHubHostname('')).toBe(false); + }); + + it('rejects null', () => { + expect(validateEventHubHostname(null as any)).toBe(false); + }); + + it('rejects undefined', () => { + expect(validateEventHubHostname(undefined as any)).toBe(false); + }); + + it('rejects bare suffix with no namespace', () => { + expect(validateEventHubHostname('.servicebus.windows.net')).toBe(false); + }); + + it('rejects namespace starting with hyphen', () => { + expect(validateEventHubHostname('-mynamespace.servicebus.windows.net')).toBe(false); + }); + + it('rejects namespace ending with hyphen', () => { + expect(validateEventHubHostname('mynamespace-.servicebus.windows.net')).toBe(false); + }); + }); +}); + +describe('extractEventHubHostname', () => { + it('extracts hostname from valid connection string', () => { + const connStr = 'Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=dGVzdA=='; + expect(extractEventHubHostname(connStr)).toBe('mynamespace.servicebus.windows.net'); + }); + + it('extracts hostname without trailing slash', () => { + const connStr = 'Endpoint=sb://mynamespace.servicebus.windows.net;SharedAccessKeyName=test;SharedAccessKey=dGVzdA=='; + expect(extractEventHubHostname(connStr)).toBe('mynamespace.servicebus.windows.net'); + }); + + it('extracts hostname from attacker connection string', () => { + const connStr = 'Endpoint=sb://attacker-controlled-host.com;SharedAccessKeyName=test;SharedAccessKey=dGVzdA=='; + expect(extractEventHubHostname(connStr)).toBe('attacker-controlled-host.com'); + }); + + it('throws on empty string', () => { + expect(() => extractEventHubHostname('')).toThrow('missing or empty'); + }); + + it('throws on null', () => { + expect(() => extractEventHubHostname(null as any)).toThrow('missing or empty'); + }); + + it('throws on connection string without Endpoint', () => { + expect(() => extractEventHubHostname('SharedAccessKeyName=test;SharedAccessKey=dGVzdA==')).toThrow('unable to extract'); + }); + + it('is case-insensitive for Endpoint prefix', () => { + const connStr = 'endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=test;SharedAccessKey=dGVzdA=='; + expect(extractEventHubHostname(connStr)).toBe('mynamespace.servicebus.windows.net'); + }); +}); diff --git a/public/handlers/urlValidator.ts b/public/handlers/urlValidator.ts index fa5a606f..e574fd09 100644 --- a/public/handlers/urlValidator.ts +++ b/public/handlers/urlValidator.ts @@ -6,6 +6,9 @@ // Allowed Azure IoT Hub domain suffix const ALLOWED_DOMAIN_SUFFIX = '.azure-devices.net'; +// Allowed Azure Event Hubs domain suffix +const ALLOWED_EVENTHUB_DOMAIN_SUFFIX = '.servicebus.windows.net'; + // Allowlist of headers that can be passed through from client const ALLOWED_HEADERS = new Set([ 'content-type', @@ -28,7 +31,7 @@ const BLOCKED_HEADERS = new Set([ ]); /** - * Validates hostname is exactly *.azure-devices.net + * Validates hostname is *.azure-devices.net or *.privatelink.azure-devices.net * - No path components (no slashes) * - No special characters except dots and hyphens in valid positions * - Each label follows DNS naming rules @@ -59,16 +62,25 @@ export function validateAzureIoTHostname(hostname: string): boolean { // Split into labels and validate each // e.g., 'myhub.azure-devices.net' -> ['myhub', 'azure-devices', 'net'] + // e.g., 'myhub.privatelink.azure-devices.net' -> ['myhub', 'privatelink', 'azure-devices', 'net'] const labels = normalizedHost.split('.'); - // Must have exactly 3 labels: .azure-devices.net - if (labels.length !== 3) { + // Must have exactly 3 labels (.azure-devices.net) + // or exactly 4 labels (.privatelink.azure-devices.net) + if (labels.length !== 3 && labels.length !== 4) { return false; } - // Verify the domain is exactly 'azure-devices.net' - if (labels[1] !== 'azure-devices' || labels[2] !== 'net') { - return false; + if (labels.length === 3) { + // Verify the domain is exactly 'azure-devices.net' + if (labels[1] !== 'azure-devices' || labels[2] !== 'net') { + return false; + } + } else { + // 4 labels: second must be 'privatelink' + if (labels[1] !== 'privatelink' || labels[2] !== 'azure-devices' || labels[3] !== 'net') { + return false; + } } // Validate hub name (first label) follows DNS naming rules: @@ -89,6 +101,81 @@ export function validateAzureIoTHostname(hostname: string): boolean { return true; } +/** + * Validates hostname is *.servicebus.windows.net or *.privatelink.servicebus.windows.net + * - No path components (no slashes) + * - No special characters except dots and hyphens in valid positions + * - Each label follows DNS naming rules + * - Must end with .servicebus.windows.net + */ +export function validateEventHubHostname(hostname: string): boolean { + if (!hostname || typeof hostname !== 'string') { + return false; + } + + const normalizedHost = hostname.toLowerCase().trim(); + + if (!normalizedHost.endsWith(ALLOWED_EVENTHUB_DOMAIN_SUFFIX)) { + return false; + } + + // Check for path injection + if (/[\/\\%]/.test(normalizedHost)) { + return false; + } + + // Check for dangerous characters + if (/[@#\?\&\=\:]/.test(normalizedHost)) { + return false; + } + + // e.g., 'mynamespace.servicebus.windows.net' -> ['mynamespace', 'servicebus', 'windows', 'net'] + // e.g., 'mynamespace.privatelink.servicebus.windows.net' -> ['mynamespace', 'privatelink', 'servicebus', 'windows', 'net'] + const labels = normalizedHost.split('.'); + + if (labels.length !== 4 && labels.length !== 5) { + return false; + } + + if (labels.length === 4) { + if (labels[1] !== 'servicebus' || labels[2] !== 'windows' || labels[3] !== 'net') { + return false; + } + } else { + if (labels[1] !== 'privatelink' || labels[2] !== 'servicebus' || labels[3] !== 'windows' || labels[4] !== 'net') { + return false; + } + } + + const namespaceName = labels[0]; + const labelRegex = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/; + + if (namespaceName.length === 0 || namespaceName.length > 63) { + return false; + } + + if (!labelRegex.test(namespaceName)) { + return false; + } + + return true; +} + +/** + * Extract hostname from an EventHub connection string of the form: + * Endpoint=sb:///;SharedAccessKeyName=...;SharedAccessKey=... + */ +export function extractEventHubHostname(connectionString: string): string { + if (!connectionString || typeof connectionString !== 'string') { + throw new Error('Invalid EventHub connection string: missing or empty'); + } + const match = connectionString.match(/Endpoint=sb:\/\/([^/;\s]+)/i); + if (!match || !match[1]) { + throw new Error('Invalid EventHub connection string: unable to extract Endpoint hostname'); + } + return match[1]; +} + /** * Sanitize headers from client request * Only allows safe headers through, blocks dangerous ones diff --git a/public/tsconfig.json b/public/tsconfig.json index 8d4cd6c7..4c35f92d 100644 --- a/public/tsconfig.json +++ b/public/tsconfig.json @@ -16,6 +16,7 @@ "contextBridge.ts", "factories/**", "utils/invokeHelper.ts", - "interfaces/**" + "interfaces/**", + "**/*.spec.ts" ] } \ No newline at end of file diff --git a/public/utils/connStringHelper.ts b/public/utils/connStringHelper.ts index e4646792..d1edbb47 100644 --- a/public/utils/connStringHelper.ts +++ b/public/utils/connStringHelper.ts @@ -17,6 +17,7 @@ import { Buffer } from "buffer"; import { AmqpError, Connection, ReceiverEvents, parseConnectionString } from "rhea-promise"; import * as rheaPromise from "rhea-promise"; import { ErrorNameConditionMapper as AMQPError } from "@azure/core-amqp"; +import { validateAzureIoTHostname, validateEventHubHostname } from "../handlers/urlValidator"; /** * Type guard for AmqpError. @@ -66,6 +67,11 @@ export async function convertIotHubToEventHubsConnectionString(connectionString: throw new Error(`Invalid IotHub connection string.`); } + // Validate hostname to prevent SSRF + if (!validateAzureIoTHostname(HostName)) { + throw new Error('Invalid IoT Hub hostname: must be a valid Azure IoT Hub endpoint (*.azure-devices.net)'); + } + //Extract the IotHub name from the hostname. const [iotHubName] = HostName.split("."); @@ -109,6 +115,8 @@ export async function convertIotHubToEventHubsConnectionString(connectionString: const regexResults = regex.exec(iotAddress); if (!hostname || !regexResults) { reject(error); + } else if (!validateEventHubHostname(hostname)) { + reject(new Error('Invalid EventHub redirect hostname: must be a valid Azure Event Hubs endpoint (*.servicebus.windows.net)')); } else { const eventHubName = regexResults[1]; resolve( diff --git a/src/app/shared/utils/hubConnectionStringHelper.spec.ts b/src/app/shared/utils/hubConnectionStringHelper.spec.ts index 5ccf37a3..8daf3ab3 100644 --- a/src/app/shared/utils/hubConnectionStringHelper.spec.ts +++ b/src/app/shared/utils/hubConnectionStringHelper.spec.ts @@ -25,10 +25,22 @@ describe('hubConnectionStringHelper', () => { it('validates event hub connection string', () => { expect(isValidEventHubConnectionString(null)).toEqual(true); - expect(isValidEventHubConnectionString('Endpoint=sb://123/;SharedAccessKeyName=456;SharedAccessKey=789')).toEqual(true); + expect(isValidEventHubConnectionString('Endpoint=sb://mynamespace.servicebus.windows.net/;SharedAccessKeyName=456;SharedAccessKey=789')).toEqual(true); + expect(isValidEventHubConnectionString('Endpoint=sb://my-ns.privatelink.servicebus.windows.net/;SharedAccessKeyName=456;SharedAccessKey=789')).toEqual(true); expect(isValidEventHubConnectionString('Endpoint=sb://123/;SharedAccessKeyName=456;SharedAccess=789')).toEqual(false); }); + it('rejects event hub connection string with attacker hostname', () => { + expect(isValidEventHubConnectionString('Endpoint=sb://evil.com/;SharedAccessKeyName=456;SharedAccessKey=789')).toEqual(false); + expect(isValidEventHubConnectionString('Endpoint=sb://attacker-controlled-host.com;SharedAccessKeyName=test;SharedAccessKey=dGVzdA==')).toEqual(false); + }); + + it('rejects event hub connection string with spoofed hostname suffix', () => { + expect(isValidEventHubConnectionString('Endpoint=sb://ns.servicebus.windows.net.evil.com/;SharedAccessKeyName=x;SharedAccessKey=y')).toEqual(false); + expect(isValidEventHubConnectionString('Endpoint=sb://ns.servicebus.windows.netEVIL/;SharedAccessKeyName=x;SharedAccessKey=y')).toEqual(false); + expect(isValidEventHubConnectionString('Endpoint=sb://ns.servicebus.windows.net/evil.com;SharedAccessKeyName=x;SharedAccessKey=y')).toEqual(false); + }); + it('formats connection strings', () => { const connectionStrings = []; for (let i = CONNECTION_STRING_LIST_MAX_LENGTH; i > 0; i--) { diff --git a/src/app/shared/utils/hubConnectionStringHelper.ts b/src/app/shared/utils/hubConnectionStringHelper.ts index c21179d8..7290379b 100644 --- a/src/app/shared/utils/hubConnectionStringHelper.ts +++ b/src/app/shared/utils/hubConnectionStringHelper.ts @@ -38,7 +38,7 @@ export const isValidEventHubConnectionString = (connectionString: string): boole if (!connectionString) { return true; } - const pattern = new RegExp('^Endpoint=sb://.*;SharedAccessKeyName=.*;SharedAccessKey=.*$'); + const pattern = new RegExp('^Endpoint=sb://[\\w\\-]+(\\.(privatelink))?\\.servicebus\\.windows\\.net\\/?;SharedAccessKeyName=.*;SharedAccessKey=.*$'); return pattern.test(connectionString); };