From b6cd9d9792e95264e41ac34553ca17358fd534f2 Mon Sep 17 00:00:00 2001 From: Revopush Date: Tue, 6 Oct 2026 20:12:39 +0300 Subject: [PATCH 1/5] fix(deps): replace proxy-agent to drop basic-ftp (CVE-2026-102990) proxy-agent always installs pac-proxy-agent -> get-uri -> basic-ftp, and even the latest get-uri still requires basic-ftp ^5, which is vulnerable to CVE-2026-102990. Consumers of @revopush/code-push inherited it with no way to fix it from their side. We only ever pass a fixed proxy URL, so use the concrete agents directly, selected the same way proxy-agent did: http(s) proxies use HttpProxyAgent/HttpsProxyAgent depending on the target, socks* proxies use SocksProxyAgent. Same majors proxy-agent 6.5 resolved, so Node support is unchanged. PAC proxy URLs (pac+...) are no longer supported. Bump version to 4.6.1. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 215 +++-------------------------------- package.json | 6 +- src/utils/request-manager.ts | 36 +++++- 3 files changed, 54 insertions(+), 203 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1932e9a8..3a95babb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,19 @@ { "name": "@revopush/code-push", - "version": "4.6.0", + "version": "4.6.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@revopush/code-push", - "version": "4.6.0", + "version": "4.6.1", "license": "MIT", "dependencies": { - "proxy-agent": "^6.3.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "recursive-fs": "^2.1.0", "slash": "^3.0.0", + "socks-proxy-agent": "^8.0.5", "superagent": "^10.3.0", "yazl": "^2.5.1" }, @@ -73,11 +75,6 @@ "node": ">=14" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==" - }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -137,6 +134,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", "engines": { "node": ">= 14" } @@ -179,17 +177,6 @@ "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -201,14 +188,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/brace-expansion": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", @@ -449,14 +428,6 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "engines": { - "node": ">= 14" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -485,19 +456,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -611,54 +569,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -794,19 +704,6 @@ "node": ">= 0.4" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/glob": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", @@ -897,6 +794,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -909,6 +807,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -944,9 +843,9 @@ } }, "node_modules/ip-address": { - "version": "10.7.2", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.2.tgz", - "integrity": "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "license": "MIT", "engines": { "node": ">= 12" @@ -1207,14 +1106,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -1264,36 +1155,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -1355,37 +1216,6 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "engines": { - "node": ">=12" - } - }, - "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==" - }, "node_modules/qs": { "version": "6.16.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", @@ -1668,15 +1498,17 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" } }, "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", "dependencies": { "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" @@ -1690,6 +1522,7 @@ "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -1699,15 +1532,6 @@ "node": ">= 14" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -1874,11 +1698,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index 25d95c3f..4bba8953 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@revopush/code-push", - "version": "4.6.0", + "version": "4.6.1", "description": "Management SDK for the CodePush service", "main": "script/index.js", "types": "script/index.d.ts", @@ -24,9 +24,11 @@ "license": "MIT", "homepage": "https://github.com/revopush/code-push", "dependencies": { - "proxy-agent": "^6.3.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "recursive-fs": "^2.1.0", "slash": "^3.0.0", + "socks-proxy-agent": "^8.0.5", "superagent": "^10.3.0", "yazl": "^2.5.1" }, diff --git a/src/utils/request-manager.ts b/src/utils/request-manager.ts index e9b18ca5..d93a318c 100644 --- a/src/utils/request-manager.ts +++ b/src/utils/request-manager.ts @@ -1,5 +1,8 @@ import superagent = require("superagent"); -import { ProxyAgent } from "proxy-agent"; +import { Agent } from "http"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; +import { SocksProxyAgent } from "socks-proxy-agent"; import { CodePushUnauthorizedError } from "../script/code-push-error" import { CodePushError, Headers } from "../script/types"; @@ -49,10 +52,16 @@ class RequestManager { private makeApiRequest(method: string, endpoint: string, requestBody: string, expectResponseBody: boolean, contentType: string): Promise { return new Promise((resolve, reject) => { - var request: superagent.Request = (superagent)[method](this._serverUrl + endpoint); + var url: string = this._serverUrl + endpoint; + var request: superagent.Request = (superagent)[method](url); if (this._proxy) { - (request).agent(new ProxyAgent({ getProxyForUrl: () => this._proxy })) + try { + (request).agent(RequestManager.createProxyAgent(this._proxy, url)); + } catch (err) { + reject(this.getCodePushError(err)); + return; + } } this.attachCredentials(request); @@ -96,6 +105,27 @@ class RequestManager { }) } + // Picks the agent the same way proxy-agent did, minus PAC support: proxy-agent + // pulled in pac-proxy-agent -> get-uri -> basic-ftp for every consumer. + private static createProxyAgent(proxy: string, targetUrl: string): Agent { + const proxyProtocol = new URL(proxy).protocol.replace(":", ""); + const secureEndpoint = new URL(targetUrl).protocol === "https:"; + + switch (proxyProtocol) { + case "http": + case "https": + return secureEndpoint ? new HttpsProxyAgent(proxy) : new HttpProxyAgent(proxy); + case "socks": + case "socks4": + case "socks4a": + case "socks5": + case "socks5h": + return new SocksProxyAgent(proxy); + default: + throw new Error(`Unsupported protocol for proxy URL: ${proxy}`); + } + } + private getCodePushError(error: any, response?: superagent.Response): CodePushError { if (error.syscall === "getaddrinfo") { error.message = `Unable to connect to the CodePush server. Are you offline, or behind a firewall or proxy?\n(${error.message})`; From afc4e4ca744d4190c294bfc334c00ad6fc3ba72a Mon Sep 17 00:00:00 2001 From: Revopush Date: Tue, 6 Oct 2026 20:33:39 +0300 Subject: [PATCH 2/5] fix: re-pick proxy agent on redirects, cache agents, add proxy tests - superagent keeps the agent across redirects, so an http -> https redirect would go through HttpProxyAgent; re-pick on "redirect". - Build the http/https agents once per RequestManager instead of per call. - Add tests for proxy agent selection, reuse and PAC rejection. Co-Authored-By: Claude Opus 5.5 --- src/test/request-manager.ts | 38 ++++++++++++++++++++++++++++ src/utils/request-manager.ts | 49 +++++++++++++++++++++++------------- 2 files changed, 70 insertions(+), 17 deletions(-) create mode 100644 src/test/request-manager.ts diff --git a/src/test/request-manager.ts b/src/test/request-manager.ts new file mode 100644 index 00000000..9619b53e --- /dev/null +++ b/src/test/request-manager.ts @@ -0,0 +1,38 @@ +import assert from "assert"; +import { HttpProxyAgent } from "http-proxy-agent"; +import { HttpsProxyAgent } from "https-proxy-agent"; +import { SocksProxyAgent } from "socks-proxy-agent"; + +import RequestManager = require("../utils/request-manager"); + +function proxyAgentFor(proxy: string, targetUrl: string): any { + return (new RequestManager("dummyAccessKey", null, "http://localhost", proxy)).getProxyAgent(targetUrl); +} + +describe("Request Manager proxy agents", () => { + it("uses HttpProxyAgent for http targets and HttpsProxyAgent for https targets behind an http(s) proxy", () => { + for (const proxy of ["http://proxy.local:8080", "https://proxy.local:8443"]) { + assert(proxyAgentFor(proxy, "http://example.com/x") instanceof HttpProxyAgent); + assert(proxyAgentFor(proxy, "https://example.com/x") instanceof HttpsProxyAgent); + } + }); + + it("uses SocksProxyAgent for socks proxies", () => { + for (const protocol of ["socks", "socks4", "socks4a", "socks5", "socks5h"]) { + assert(proxyAgentFor(`${protocol}://proxy.local:1080`, "https://example.com/x") instanceof SocksProxyAgent); + } + }); + + it("reuses the same agents across requests", () => { + const manager: any = new RequestManager("dummyAccessKey", null, "http://localhost", "http://proxy.local:8080"); + assert.strictEqual(manager.getProxyAgent("https://example.com/a"), manager.getProxyAgent("https://example.com/b")); + }); + + it("rejects requests when the proxy protocol is unsupported", () => { + const manager = new RequestManager("dummyAccessKey", null, "http://localhost", "pac+http://proxy.local/proxy.pac"); + return manager.get("/endpoint").then( + () => assert.fail("Expected the request to be rejected"), + (error: any) => assert(/Unsupported protocol for proxy URL/.test(error.message)) + ); + }); +}); diff --git a/src/utils/request-manager.ts b/src/utils/request-manager.ts index d93a318c..6277f73b 100644 --- a/src/utils/request-manager.ts +++ b/src/utils/request-manager.ts @@ -11,6 +11,11 @@ interface JsonResponse { body?: any; } +interface ProxyAgents { + http: Agent; + https: Agent; +} + class RequestManager { public static SERVER_URL = "https://api.appcenter.ms/v0.1"; @@ -24,6 +29,7 @@ class RequestManager { private _serverUrl: string; private _customHeaders: Headers; private _proxy: string; + private _proxyAgents: ProxyAgents; constructor(accessKey: string, customHeaders?: Headers, serverUrl?: string, proxy?: string) { if (!accessKey) throw new CodePushUnauthorizedError("A token must be specified."); @@ -57,7 +63,10 @@ class RequestManager { if (this._proxy) { try { - (request).agent(RequestManager.createProxyAgent(this._proxy, url)); + var proxiedRequest = request; + proxiedRequest.agent(this.getProxyAgent(url)); + // superagent keeps the agent across redirects; re-pick it in case the redirect switched http <-> https. + proxiedRequest.on("redirect", () => proxiedRequest.agent(this.getProxyAgent(proxiedRequest.url))); } catch (err) { reject(this.getCodePushError(err)); return; @@ -105,22 +114,28 @@ class RequestManager { }) } - // Picks the agent the same way proxy-agent did, minus PAC support: proxy-agent - // pulled in pac-proxy-agent -> get-uri -> basic-ftp for every consumer. - private static createProxyAgent(proxy: string, targetUrl: string): Agent { - const proxyProtocol = new URL(proxy).protocol.replace(":", ""); - const secureEndpoint = new URL(targetUrl).protocol === "https:"; - - switch (proxyProtocol) { - case "http": - case "https": - return secureEndpoint ? new HttpsProxyAgent(proxy) : new HttpProxyAgent(proxy); - case "socks": - case "socks4": - case "socks4a": - case "socks5": - case "socks5h": - return new SocksProxyAgent(proxy); + private getProxyAgent(targetUrl: string): Agent { + if (!this._proxyAgents) { + this._proxyAgents = RequestManager.createProxyAgents(this._proxy); + } + + return new URL(targetUrl).protocol === "https:" ? this._proxyAgents.https : this._proxyAgents.http; + } + + // Mirrors proxy-agent's selection without PAC support; proxy-agent pulls in basic-ftp via pac-proxy-agent -> get-uri. + private static createProxyAgents(proxy: string): ProxyAgents { + switch (new URL(proxy).protocol) { + case "http:": + case "https:": + return { http: new HttpProxyAgent(proxy), https: new HttpsProxyAgent(proxy) }; + case "socks:": + case "socks4:": + case "socks4a:": + case "socks5:": + case "socks5h:": { + const socksAgent = new SocksProxyAgent(proxy); + return { http: socksAgent, https: socksAgent }; + } default: throw new Error(`Unsupported protocol for proxy URL: ${proxy}`); } From b3992561e1da25cee86c862b88d52e587de80304 Mon Sep 17 00:00:00 2001 From: Revopush Date: Tue, 6 Oct 2026 22:35:48 +0300 Subject: [PATCH 3/5] chore: bump version to 4.7.0 Dropping PAC proxy URL support is a behaviour change, so release it as a minor rather than a patch. Co-Authored-By: Claude Opus 5.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a95babb..68f74f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@revopush/code-push", - "version": "4.6.1", + "version": "4.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@revopush/code-push", - "version": "4.6.1", + "version": "4.7.0", "license": "MIT", "dependencies": { "http-proxy-agent": "^7.0.2", diff --git a/package.json b/package.json index 4bba8953..3b7ebbda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@revopush/code-push", - "version": "4.6.1", + "version": "4.7.0", "description": "Management SDK for the CodePush service", "main": "script/index.js", "types": "script/index.d.ts", From 3335cfbd3f44f3bb55ec6f6f2fab42fcc58121c4 Mon Sep 17 00:00:00 2001 From: Revopush Date: Tue, 6 Oct 2026 22:42:50 +0300 Subject: [PATCH 4/5] fix: hide proxy credentials in errors, test redirect agent re-pick - Strip username/password from the unsupported-protocol error, since the CLI prints it. - Add a test with a local proxy that checks an http -> https redirect re-picks the agent (fails without the redirect listener). - Narrow the try block to the call that can throw; tighten comments. Co-Authored-By: Claude Opus 5.5 --- src/test/request-manager.ts | 41 ++++++++++++++++++++++++++++++++++++ src/utils/request-manager.ts | 17 +++++++++------ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/test/request-manager.ts b/src/test/request-manager.ts index 9619b53e..312b6dac 100644 --- a/src/test/request-manager.ts +++ b/src/test/request-manager.ts @@ -1,4 +1,6 @@ import assert from "assert"; +import * as http from "http"; +import { AddressInfo } from "net"; import { HttpProxyAgent } from "http-proxy-agent"; import { HttpsProxyAgent } from "https-proxy-agent"; import { SocksProxyAgent } from "socks-proxy-agent"; @@ -35,4 +37,43 @@ describe("Request Manager proxy agents", () => { (error: any) => assert(/Unsupported protocol for proxy URL/.test(error.message)) ); }); + + it("does not leak proxy credentials in the unsupported protocol error", () => { + const manager = new RequestManager("dummyAccessKey", null, "http://localhost", "pac+http://user:secret@proxy.local/proxy.pac"); + return manager.get("/endpoint").then( + () => assert.fail("Expected the request to be rejected"), + (error: any) => { + assert(/Unsupported protocol for proxy URL/.test(error.message)); + assert(!/secret/.test(error.message), `Proxy password leaked: ${error.message}`); + } + ); + }); + + it("re-picks the proxy agent when a redirect switches from http to https", () => { + const connectTargets: string[] = []; + // Plain-HTTP requests reach the proxy directly: redirect them to https. + const proxy = http.createServer((req, res) => { + res.writeHead(302, { Location: "https://redirected.invalid/target" }); + res.end(); + }); + // HTTPS requests tunnel via CONNECT: record the target, then drop the tunnel. + proxy.on("connect", (req: http.IncomingMessage, socket: any) => { + connectTargets.push(req.url); + socket.destroy(); + }); + + const closeProxy = () => new Promise((resolve) => proxy.close(() => resolve())); + + return new Promise((resolve) => proxy.listen(0, "127.0.0.1", () => resolve())) + .then(() => { + const proxyUrl = `http://127.0.0.1:${(proxy.address()).port}`; + const manager = new RequestManager("dummyAccessKey", null, "http://origin.invalid", proxyUrl); + return manager.get("/start").then( + () => assert.fail("Expected the tunnelled request to fail"), + () => { /* the proxy drops the tunnel */ } + ); + }) + .then(() => assert.deepStrictEqual(connectTargets, ["redirected.invalid:443"])) + .then(closeProxy, (err: any) => closeProxy().then(() => { throw err; })); + }); }); diff --git a/src/utils/request-manager.ts b/src/utils/request-manager.ts index 6277f73b..9ccb48ad 100644 --- a/src/utils/request-manager.ts +++ b/src/utils/request-manager.ts @@ -62,15 +62,16 @@ class RequestManager { var request: superagent.Request = (superagent)[method](url); if (this._proxy) { + var proxiedRequest = request; try { - var proxiedRequest = request; proxiedRequest.agent(this.getProxyAgent(url)); - // superagent keeps the agent across redirects; re-pick it in case the redirect switched http <-> https. - proxiedRequest.on("redirect", () => proxiedRequest.agent(this.getProxyAgent(proxiedRequest.url))); } catch (err) { reject(this.getCodePushError(err)); return; } + + // superagent reuses the agent on redirect, so re-pick it in case the scheme changed. + proxiedRequest.on("redirect", () => proxiedRequest.agent(this.getProxyAgent(proxiedRequest.url))); } this.attachCredentials(request); @@ -122,9 +123,10 @@ class RequestManager { return new URL(targetUrl).protocol === "https:" ? this._proxyAgents.https : this._proxyAgents.http; } - // Mirrors proxy-agent's selection without PAC support; proxy-agent pulls in basic-ftp via pac-proxy-agent -> get-uri. + // Same agent selection as proxy-agent, minus PAC (its dependencies pull in basic-ftp). private static createProxyAgents(proxy: string): ProxyAgents { - switch (new URL(proxy).protocol) { + const proxyUrl = new URL(proxy); + switch (proxyUrl.protocol) { case "http:": case "https:": return { http: new HttpProxyAgent(proxy), https: new HttpsProxyAgent(proxy) }; @@ -137,7 +139,10 @@ class RequestManager { return { http: socksAgent, https: socksAgent }; } default: - throw new Error(`Unsupported protocol for proxy URL: ${proxy}`); + // Strip credentials: the CLI prints this message. + proxyUrl.username = ""; + proxyUrl.password = ""; + throw new Error(`Unsupported protocol for proxy URL: ${proxyUrl.href}`); } } From d22feca7ee1b8489f185cb9b5e2b8cfd809439cc Mon Sep 17 00:00:00 2001 From: Revopush Date: Tue, 6 Oct 2026 22:44:19 +0300 Subject: [PATCH 5/5] fix: hide proxy credentials when the proxy URL is malformed new URL() throws before the credential stripping runs, and older Node versions include the whole input in that error message. Replace it with a generic "Invalid proxy URL" error. Co-Authored-By: Claude Opus 5.5 --- src/test/request-manager.ts | 8 ++++++++ src/utils/request-manager.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/test/request-manager.ts b/src/test/request-manager.ts index 312b6dac..2c63adf4 100644 --- a/src/test/request-manager.ts +++ b/src/test/request-manager.ts @@ -49,6 +49,14 @@ describe("Request Manager proxy agents", () => { ); }); + it("does not leak proxy credentials when the proxy URL is malformed", () => { + const manager = new RequestManager("dummyAccessKey", null, "http://localhost", "http://user:secret@[proxy.local:8080"); + return manager.get("/endpoint").then( + () => assert.fail("Expected the request to be rejected"), + (error: any) => assert.strictEqual(error.message, "Invalid proxy URL") + ); + }); + it("re-picks the proxy agent when a redirect switches from http to https", () => { const connectTargets: string[] = []; // Plain-HTTP requests reach the proxy directly: redirect them to https. diff --git a/src/utils/request-manager.ts b/src/utils/request-manager.ts index 9ccb48ad..f135aa81 100644 --- a/src/utils/request-manager.ts +++ b/src/utils/request-manager.ts @@ -125,7 +125,14 @@ class RequestManager { // Same agent selection as proxy-agent, minus PAC (its dependencies pull in basic-ftp). private static createProxyAgents(proxy: string): ProxyAgents { - const proxyUrl = new URL(proxy); + let proxyUrl: URL; + try { + proxyUrl = new URL(proxy); + } catch { + // Older Node versions put the whole input, credentials included, in the error message. + throw new Error("Invalid proxy URL"); + } + switch (proxyUrl.protocol) { case "http:": case "https:":