From d0a8d448be5c0297b13f8b5f1432a268f72a0545 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 10:50:48 +0000 Subject: [PATCH] fix(seguridad): lockfile en sync, fuera localtunnel, receptores en loopback, tests en CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - package-lock.json se desincronizó (faltaba ws@8.21.3 de @openai/agents-core) y `npm ci` fallaba en CI y en release. Regenerado. - localtunnel no se importa en ningún lado y arrastraba axios vulnerable (varios GHSA high). Eliminado junto con @types/localtunnel. - hono sube a 4.13.9 (npm audit fix, sin breaking). - WebhookServer y SlackWebhookServer escuchaban en todas las interfaces; tunnelmole reenvía a localhost, así que ahora se bindean a 127.0.0.1 como ya hacían controlChannel e integrationBroker. Test nuevo. - CI corre la suite de tests (test:focused + tests del CLI munder). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0141jpRihLcQWr34jq6mvC7b --- .github/workflows/ci.yml | 19 ++++ package-lock.json | 145 ++++++------------------ package.json | 2 - src/main/slack.ts | 4 +- src/main/webhook.ts | 4 +- test/public-listeners-loopback.test.cjs | 42 +++++++ 6 files changed, 103 insertions(+), 113 deletions(-) create mode 100644 test/public-listeners-loopback.test.cjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e20b8c8..cf8f7294 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,3 +40,22 @@ jobs: cache: npm - run: npm ci - run: npm run build + + test: + name: Tests + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + # npm ci runs postinstall (electron-rebuild), so node-pty is built for the + # tests that spawn real PTYs; the build gives start.sh --check its bundle. + - run: npm ci + - run: npm run build + - name: Focused tests + run: npm run test:focused + # proveedor.test.cjs is left out: it prompts for sudo and hangs headless. + - name: munder CLI tests + run: node --test tools/munder/link.test.cjs tools/munder/avatar.test.cjs diff --git a/package-lock.json b/package-lock.json index 13077389..66d151e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,6 @@ "commit-graph": "^2.4.0", "electron-updater": "^6.8.9", "i18next": "^26.3.6", - "localtunnel": "^2.0.2", "monaco-editor": "^0.52.2", "node-pty": "^1.0.0", "pixi.js": "^8.5.1", @@ -48,7 +47,6 @@ "@electron/notarize": "^2.5.0", "@electron/rebuild": "^3.7.0", "@types/better-sqlite3": "^7.6.13", - "@types/localtunnel": "^2.0.4", "@types/node": "^22.7.5", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", @@ -2008,6 +2006,29 @@ } } }, + "node_modules/@openai/agents-core/node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@openai/agents-realtime": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@openai/agents-realtime/-/agents-realtime-0.11.8.tgz", @@ -2664,16 +2685,6 @@ "@types/node": "*" } }, - "node_modules/@types/localtunnel": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/localtunnel/-/localtunnel-2.0.4.tgz", - "integrity": "sha512-7WM5nlEfEKp8MpwthPa2utdy+f/7ZBxMPzu8qw6EijFFTcpzh5CXgt2YoncxWAZNOPNieMofXCKFudtDEY4bag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -3118,6 +3129,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3492,15 +3504,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -5523,6 +5526,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -5693,6 +5697,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -6281,6 +6286,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -6613,9 +6619,9 @@ } }, "node_modules/hono": { - "version": "4.13.3", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.3.tgz", - "integrity": "sha512-r8AO2mYHoLxSHkgafNeC/BXyb2vWRxD3jem4Ts+ptav8oTG5FIRifAjuJEmZI4bSvvc2ns0GxmIYiZnHqN3mMw==", + "version": "4.13.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.9.tgz", + "integrity": "sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg==", "license": "MIT", "optional": true, "engines": { @@ -7083,6 +7089,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7607,85 +7614,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/localtunnel": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz", - "integrity": "sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug==", - "license": "MIT", - "dependencies": { - "axios": "0.21.4", - "debug": "4.3.2", - "openurl": "1.1.1", - "yargs": "17.1.1" - }, - "bin": { - "lt": "bin/lt.js" - }, - "engines": { - "node": ">=8.3.0" - } - }, - "node_modules/localtunnel/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/localtunnel/node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/localtunnel/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "license": "MIT" - }, - "node_modules/localtunnel/node_modules/yargs": { - "version": "17.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz", - "integrity": "sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ==", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/localtunnel/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/lodash": { "version": "4.18.1", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", @@ -9303,12 +9231,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openurl": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz", - "integrity": "sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA==", - "license": "MIT" - }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -10209,6 +10131,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11046,6 +10969,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -11090,6 +11014,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -11945,6 +11870,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -12038,6 +11964,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/package.json b/package.json index f0cf1f6b..0f51ed47 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "commit-graph": "^2.4.0", "electron-updater": "^6.8.9", "i18next": "^26.3.6", - "localtunnel": "^2.0.2", "monaco-editor": "^0.52.2", "node-pty": "^1.0.0", "pixi.js": "^8.5.1", @@ -66,7 +65,6 @@ "@electron/notarize": "^2.5.0", "@electron/rebuild": "^3.7.0", "@types/better-sqlite3": "^7.6.13", - "@types/localtunnel": "^2.0.4", "@types/node": "^22.7.5", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", diff --git a/src/main/slack.ts b/src/main/slack.ts index b99495ad..923acb56 100644 --- a/src/main/slack.ts +++ b/src/main/slack.ts @@ -176,7 +176,9 @@ export class SlackWebhookServer { const server = createServer((req, res) => this.handleRequest(req, res)); const onError = (e: Error): void => reject(e); server.once('error', onError); - server.listen(this.port, () => { + // Loopback only: tunnelmole forwards to localhost, so nothing else needs + // to reach this port. Binding every interface exposed it to the LAN too. + server.listen(this.port, '127.0.0.1', () => { server.off('error', onError); this.server = server; resolve(); diff --git a/src/main/webhook.ts b/src/main/webhook.ts index 01346f35..3db022ec 100644 --- a/src/main/webhook.ts +++ b/src/main/webhook.ts @@ -241,7 +241,9 @@ export class WebhookServer { const server = createServer((req, res) => this.handleRequest(req, res)); const onError = (e: Error): void => reject(e); server.once('error', onError); - server.listen(this.port, () => { + // Loopback only: tunnelmole forwards to localhost, so nothing else needs + // to reach this port. Binding every interface exposed it to the LAN too. + server.listen(this.port, '127.0.0.1', () => { server.off('error', onError); this.server = server; resolve(); diff --git a/test/public-listeners-loopback.test.cjs b/test/public-listeners-loopback.test.cjs new file mode 100644 index 00000000..327f1ac4 --- /dev/null +++ b/test/public-listeners-loopback.test.cjs @@ -0,0 +1,42 @@ +'use strict'; + +/** + * The webhook and Slack receivers are reached from the internet ONLY through + * tunnelmole, which forwards to localhost. Binding every interface also put + * them on the LAN, secret-gated but reachable by anyone on the network. Both + * must bind loopback. `listen()` is driven directly: `start()` would open a + * real tunnel, and a unit test must never reach the network. + */ + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const loadTs = require('./load-ts.cjs'); + +const { WebhookServer } = loadTs('src/main/webhook.ts'); +const { SlackWebhookServer } = loadTs('src/main/slack.ts'); + +async function boundAddress(server) { + await server.listen(); + try { + return server.server.address(); + } finally { + server.stop(); + } +} + +test('webhook receiver binds loopback only', async () => { + const s = new WebhookServer({ + port: 0, + endpoints: [{ id: 'a', name: 'A', secret: 'x'.repeat(32), schema: '' }], + onMessage: () => null, + lookupStatus: () => null + }); + const addr = await boundAddress(s); + assert.equal(addr.address, '127.0.0.1'); +}); + +test('Slack receiver binds loopback only', async () => { + const s = new SlackWebhookServer({ port: 0, signingSecret: 'secret', onMessage: () => {} }); + const addr = await boundAddress(s); + assert.equal(addr.address, '127.0.0.1'); +});