From 7ce63740ce6eb01a373f3291012324941c68494e Mon Sep 17 00:00:00 2001 From: rafarhat Date: Sat, 7 Feb 2026 18:24:04 -0800 Subject: [PATCH 1/3] fix: Use explicit import path for request-filtering-agent asar compatibility --- package.json | 2 +- public/handlers/dataPlaneHandler.ts | 24 +++++++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0055606c..c89f715c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "azure-iot-explorer", - "version": "0.15.13", + "version": "0.15.14", "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/dataPlaneHandler.ts b/public/handlers/dataPlaneHandler.ts index 65db2f1e..d9886e7a 100644 --- a/public/handlers/dataPlaneHandler.ts +++ b/public/handlers/dataPlaneHandler.ts @@ -20,12 +20,26 @@ let requestFilteringAgent: any = null; // tslint:disable-line:no-any /** * Dynamically import request-filtering-agent (ESM module) * Uses Function constructor to prevent TypeScript from converting to require() + * Tries bare specifier first (works in dev), then falls back to file:// URL for asar compatibility */ const getRequestFilteringAgent = async () => { if (!requestFilteringAgent) { - // Use Function constructor to create a true dynamic import that won't be transformed by TypeScript const dynamicImport = new Function('specifier', 'return import(specifier)'); - requestFilteringAgent = await dynamicImport('request-filtering-agent'); + try { + requestFilteringAgent = await dynamicImport('request-filtering-agent'); + } catch { + try { + // Fallback: resolve via file URL for asar compatibility where bare specifier may not work + const path = require('path'); + const url = require('url'); + const modulePath = path.join(__dirname, '..', '..', 'node_modules', 'request-filtering-agent', 'lib', 'request-filtering-agent.js'); + requestFilteringAgent = await dynamicImport(url.pathToFileURL(modulePath).href); + } catch (error) { + // tslint:disable-next-line:no-console + console.warn('Failed to load request-filtering-agent, SSRF protection disabled:', error); + return null; + } + } } return requestFilteringAgent; }; @@ -99,7 +113,7 @@ export const generateDataPlaneRequestBody = async (request: DataPlaneRequest) => const url = `https://${hostname}/${encodeURIComponent(path)}${queryString}`; // Dynamically import ESM module for SSRF protection - const { useAgent } = await getRequestFilteringAgent(); + const rfaModule = await getRequestFilteringAgent(); return { url, @@ -109,9 +123,9 @@ export const generateDataPlaneRequestBody = async (request: DataPlaneRequest) => method: request.httpMethod.toUpperCase(), redirect: 'error' as const, // Block all HTTP redirects (SSRF protection) timeout: 30000, // 30 second timeout - // Use request-filtering-agent for SSRF protection + // Use request-filtering-agent for SSRF protection if available // Blocks requests to private IPs, loopback, link-local, IMDS, etc. - agent: useAgent(url), + agent: rfaModule ? rfaModule.useAgent(url) : undefined, } }; }; From 2a0a47f552725a1cfc26720353c6121439187f00 Mon Sep 17 00:00:00 2001 From: rafarhat Date: Sun, 8 Feb 2026 03:45:13 +0000 Subject: [PATCH 2/3] fix: resolve SSRF filter agent ENOTDIR on Linux asar packaging On Linux, dynamic import() of ESM modules from inside .asar archives fails with ENOTDIR because the ESM loader bypasses Electron's patched fs module. Fix by: - Adding asarUnpack for request-filtering-agent and ipaddr.js so they are extracted as real files alongside the asar archive - Redirecting the fallback import path from .asar/ to .asar.unpacked/ on Linux where the ESM loader cannot resolve asar-internal paths --- package.json | 4 ++++ public/handlers/dataPlaneHandler.ts | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c89f715c..e5789ad0 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,10 @@ "directories": { "buildResources": "icon" }, + "asarUnpack": [ + "node_modules/request-filtering-agent/**", + "node_modules/ipaddr.js/**" + ], "afterPack": "./scripts/flipFuses.js", "linux": { "category": "Utility", diff --git a/public/handlers/dataPlaneHandler.ts b/public/handlers/dataPlaneHandler.ts index d9886e7a..3bb86e06 100644 --- a/public/handlers/dataPlaneHandler.ts +++ b/public/handlers/dataPlaneHandler.ts @@ -32,7 +32,14 @@ const getRequestFilteringAgent = async () => { // Fallback: resolve via file URL for asar compatibility where bare specifier may not work const path = require('path'); const url = require('url'); - const modulePath = path.join(__dirname, '..', '..', 'node_modules', 'request-filtering-agent', 'lib', 'request-filtering-agent.js'); + let modulePath = path.join(__dirname, '..', '..', 'node_modules', 'request-filtering-agent', 'lib', 'request-filtering-agent.js'); + // On Linux, ESM dynamic import doesn't go through Electron's asar patches + if (process.platform === 'linux') { + const asarSep = '.asar' + path.sep; + if (modulePath.includes(asarSep)) { + modulePath = modulePath.replace(asarSep, '.asar.unpacked' + path.sep); + } + } requestFilteringAgent = await dynamicImport(url.pathToFileURL(modulePath).href); } catch (error) { // tslint:disable-next-line:no-console From 6d2053cdc2f0981fbb7e642e4d93fee03034e180 Mon Sep 17 00:00:00 2001 From: rafarhat Date: Sun, 8 Feb 2026 04:17:23 +0000 Subject: [PATCH 3/3] Replace request-filtering-agent with ssrf-req-filter for CJS compatibility Replace ESM-only request-filtering-agent with CJS-compatible ssrf-req-filter to eliminate complex dynamic import workarounds in the Electron main process. - Remove 37-line dynamic import fallback chain (Function constructor hack, asar unpacking, silent degradation) - Replace with simple static import and direct ssrf-req-filter(url) call - Remove request-filtering-agent from asarUnpack (CJS works inside asar) - Remove request-filtering-agent from Jest transformIgnorePatterns - Same SSRF protection: DNS lookup + IP validation via ipaddr.js --- package-lock.json | 94 +++++++---------------------- package.json | 10 +-- public/handlers/dataPlaneHandler.ts | 47 ++------------- 3 files changed, 28 insertions(+), 123 deletions(-) diff --git a/package-lock.json b/package-lock.json index c2d19db2..aa525366 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "azure-iot-explorer", - "version": "0.15.13", + "version": "0.15.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "azure-iot-explorer", - "version": "0.15.13", + "version": "0.15.14", "license": "MIT", "dependencies": { "@azure/core-amqp": "^3.2.0", @@ -44,8 +44,8 @@ "react-router-dom": "^5.2.0", "react-toastify": "^4.4.0", "redux-saga": "^1.1.3", - "request-filtering-agent": "^3.2.0", "semver": "^6.3.1", + "ssrf-req-filter": "^1.1.1", "typescript-fsa": "^3.0.0-beta-2", "typescript-fsa-reducers": "^1.0.0", "uuid": "^3.3.3", @@ -86,7 +86,6 @@ "enzyme-adapter-react-16": "^1.15.1", "enzyme-to-json": "^3.3.5", "html-webpack-plugin": "^5.5.0", - "is-svg": "^4.4.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-plugin-context": "^2.9.0", @@ -10696,25 +10695,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.1.1" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/fastest-levenshtein": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", @@ -12592,22 +12572,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-svg": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.4.0.tgz", - "integrity": "sha512-v+AgVwiK5DsGtT9ng+m4mClp6zDAmwrW8nZi6Gg15qzvBnRWWdfWA1TGaXyCDnWq5g5asofIgMVl3PjKxvk1ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-xml-parser": "^4.1.3" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-symbol": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", @@ -19044,27 +19008,6 @@ "entities": "^2.0.0" } }, - "node_modules/request-filtering-agent": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/request-filtering-agent/-/request-filtering-agent-3.2.0.tgz", - "integrity": "sha512-tKPrKdsmTFuGG1/pBEpzTB66mDZ2lZLW8kjW4N6jj4QjnxUTKrIfv5p2zuJRfztOos86jRPD41lRaGjh+1QqDw==", - "license": "MIT", - "dependencies": { - "ipaddr.js": "^2.1.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/request-filtering-agent/node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -20379,6 +20322,24 @@ "license": "BSD-3-Clause", "optional": true }, + "node_modules/ssrf-req-filter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ssrf-req-filter/-/ssrf-req-filter-1.1.1.tgz", + "integrity": "sha512-z0duj5tuvr5YetayhTdQWHf5gEncHvY6nL1HoqiFaF0gfK8Gc3VpTlxvAf4DaOVwcoczfqsL97H5TJRfYFtEYg==", + "license": "MIT", + "dependencies": { + "ipaddr.js": "^2.2.0" + } + }, + "node_modules/ssrf-req-filter/node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -20684,19 +20645,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/style-loader": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", diff --git a/package.json b/package.json index e5789ad0..532cb73e 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,6 @@ "directories": { "buildResources": "icon" }, - "asarUnpack": [ - "node_modules/request-filtering-agent/**", - "node_modules/ipaddr.js/**" - ], "afterPack": "./scripts/flipFuses.js", "linux": { "category": "Utility", @@ -115,14 +111,15 @@ "react-router-dom": "^5.2.0", "react-toastify": "^4.4.0", "redux-saga": "^1.1.3", - "request-filtering-agent": "^3.2.0", "semver": "^6.3.1", + "ssrf-req-filter": "^1.1.1", "typescript-fsa": "^3.0.0-beta-2", "typescript-fsa-reducers": "^1.0.0", "uuid": "^3.3.3", "ws": "^8.17.1" }, "devDependencies": { + "@electron/fuses": "^1.8.0", "@redux-saga/testing-utils": "^1.1.3", "@types/async-lock": "^1.1.0", "@types/core-js": "^2.5.0", @@ -152,7 +149,6 @@ "electron": "^22.3.25", "electron-builder": "^26.0.0", "electron-reload": "^2.0.0-alpha.1", - "@electron/fuses": "^1.8.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.1", "enzyme-to-json": "^3.3.5", @@ -218,7 +214,7 @@ ] }, "transformIgnorePatterns": [ - "node_modules/(?!react-movable|request-filtering-agent|cheerio|htmlparser2|dom-serializer|domhandler|domutils|entities|css-select|css-what|boolbase|nth-check|parse5|parse5-htmlparser2-tree-adapter|@azure/event-hubs|@azure/core-amqp|@azure/core-auth|@azure/core-rest-pipeline|@azure/core-tracing|@azure/core-util|@azure/abort-controller|@azure/logger|@typespec/ts-http-runtime)" + "node_modules/(?!react-movable|cheerio|htmlparser2|dom-serializer|domhandler|domutils|entities|css-select|css-what|boolbase|nth-check|parse5|parse5-htmlparser2-tree-adapter|@azure/event-hubs|@azure/core-amqp|@azure/core-auth|@azure/core-rest-pipeline|@azure/core-tracing|@azure/core-util|@azure/abort-controller|@azure/logger|@typespec/ts-http-runtime)" ], "testRegex": "(\\.|/)(spec)\\.(tsx?)$", "moduleNameMapper": { diff --git a/public/handlers/dataPlaneHandler.ts b/public/handlers/dataPlaneHandler.ts index 3bb86e06..c7c39645 100644 --- a/public/handlers/dataPlaneHandler.ts +++ b/public/handlers/dataPlaneHandler.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License **********************************************************/ import fetch, { Response } from 'node-fetch'; +import * as ssrfFilter from 'ssrf-req-filter'; import { validateAzureIoTHostname, sanitizeHeaders, @@ -14,43 +15,6 @@ import { DataPlaneRequest, DataPlaneResponse } from '../interfaces/deviceInterfa const DEVICE_STATUS_HEADER = 'x-ms-command-statuscode'; const SERVER_ERROR = 500; -// Cache the dynamically imported module -let requestFilteringAgent: any = null; // tslint:disable-line:no-any - -/** - * Dynamically import request-filtering-agent (ESM module) - * Uses Function constructor to prevent TypeScript from converting to require() - * Tries bare specifier first (works in dev), then falls back to file:// URL for asar compatibility - */ -const getRequestFilteringAgent = async () => { - if (!requestFilteringAgent) { - const dynamicImport = new Function('specifier', 'return import(specifier)'); - try { - requestFilteringAgent = await dynamicImport('request-filtering-agent'); - } catch { - try { - // Fallback: resolve via file URL for asar compatibility where bare specifier may not work - const path = require('path'); - const url = require('url'); - let modulePath = path.join(__dirname, '..', '..', 'node_modules', 'request-filtering-agent', 'lib', 'request-filtering-agent.js'); - // On Linux, ESM dynamic import doesn't go through Electron's asar patches - if (process.platform === 'linux') { - const asarSep = '.asar' + path.sep; - if (modulePath.includes(asarSep)) { - modulePath = modulePath.replace(asarSep, '.asar.unpacked' + path.sep); - } - } - requestFilteringAgent = await dynamicImport(url.pathToFileURL(modulePath).href); - } catch (error) { - // tslint:disable-next-line:no-console - console.warn('Failed to load request-filtering-agent, SSRF protection disabled:', error); - return null; - } - } - } - return requestFilteringAgent; -}; - /** * Handle data plane request via IPC * This replaces the Express route handler @@ -119,9 +83,6 @@ export const generateDataPlaneRequestBody = async (request: DataPlaneRequest) => const url = `https://${hostname}/${encodeURIComponent(path)}${queryString}`; - // Dynamically import ESM module for SSRF protection - const rfaModule = await getRequestFilteringAgent(); - return { url, request: { @@ -130,9 +91,9 @@ export const generateDataPlaneRequestBody = async (request: DataPlaneRequest) => method: request.httpMethod.toUpperCase(), redirect: 'error' as const, // Block all HTTP redirects (SSRF protection) timeout: 30000, // 30 second timeout - // Use request-filtering-agent for SSRF protection if available - // Blocks requests to private IPs, loopback, link-local, IMDS, etc. - agent: rfaModule ? rfaModule.useAgent(url) : undefined, + // Use ssrf-req-filter for SSRF protection + // Blocks requests to private IPs, loopback, link-local, etc. + agent: ssrfFilter(url), } }; };