Skip to content

Fix the undici proxy bypass and align clients, types, and CI - #80

Draft
proxymeshai wants to merge 1 commit into
mainfrom
cursor/fix-undici-tunnel-and-ci-1ad0
Draft

proxymeshai wants to merge 1 commit into
mainfrom
cursor/fix-undici-tunnel-and-ci-1ad0

Conversation

@proxymeshai

Copy link
Copy Markdown
Collaborator

This is a security fix and warrants a patch release. This PR does not bump the version, tag, or open a release/* branch. Auto-merge is off.

Verified 2026-10-22 against a local mock CONNECT proxy. The undici adapter was handing undici an already-open tunnel socket (connect: { socket }). Undici's default connector only uses socket as the HTTP socket for a TLS upgrade and otherwise dials the origin itself (undici lib/core/connect.js). On undici 7.30.1 and 8.12.2, zero bytes went through the proxy, so the caller's real IP leaked.

What changed

  1. Undici no longer bypasses the proxy. HTTPS now uses a custom undici.Client connector that calls ProxyHeadersAgent.createConnection (CONNECT, then TLS on that socket) and reads CONNECT response headers with getProxyHeadersFromSocket. Plain HTTP uses undici.ProxyAgent and its native CONNECT headers option (undici PR #1877), with a fallback for older ProxyAgent constructors. requestTls / tlsOptions are applied. test/test_proxy_traversal.js fails unless the mock proxy saw the CONNECT and tunneled bytes in both directions (a direct origin dial would still return 200 and fail this assertion).

  2. ky 2.2.0. ky 2.2 requires fetch to resolve to a real Response (instanceof globalThis.Response or [object Response]). The adapter now converts the node-fetch body to a native Response on ky 2 and keeps the previous ProxyResponse path on ky 1. proxyHeaders is an own property of that Response (clone() does not copy it).

  3. TypeScript 7. Removed baseUrl (and ignoreDeprecations) from tsconfig.json. paths stay relative to the tsconfig file. npm run test:types passes on the repo's TypeScript 7.

  4. Peer ranges and engines. Widened to the majors exercised here, and left every peer optional in peerDependenciesMeta (already optional):

    • got ^12 || ^13 || ^14 || ^15 || ^16
    • undici ^5 || ^6 || ^7 || ^8
    • make-fetch-happen ^14 || ^15 || ^16
    • typed-rest-client ^2 || ^3
    • ky ^1 || ^2 (unchanged range; ky 2 is now actually supported)
      engines.node is >=22.19.0, matching undici 8. npm i javascript-proxy-headers got@16 no longer ERESOLVEs for got 16, undici 8, make-fetch-happen 16, or typed-rest-client 3. make-fetch-happen 16's own engines are stricter (^22.22.2 || ^24.15.0 || >=26); current Node 22 and 24 releases satisfy that. Node 25 and 26 are not in the CI matrix.
  5. typed-rest-client v3. The adapter still subclasses HttpClient and overrides _getAgent (TypeScript private compiles to a normal prototype method; v3.1.2 still calls this._getAgent from _prepareRequest). A traversal test asserts the override returns proxyAgent and that CONNECT plus tunneled bytes hit the mock proxy. Checked on 2.3.1 and 3.1.2.

  6. await on async factories. README, JSDoc, and examples for createProxyAxios, createProxyGot, and createProxySuperagent now await the factory (ky and wretch examples already did). Copying the old README threw client.get is not a function. The .d.ts factories already returned Promise<...>; createProxyMakeFetchHappen and createProxyRestClient stay synchronous. types/undici.d.ts and types/ky.d.ts document tlsOptions / requestTls.

  7. Package metadata (code only). npm description and keywords now include proxy-headers, https-proxy, https-proxy-agent, proxy-agent, rotating-proxy, node-fetch, and x-proxymesh-ip. Homepage was already https://github.com/proxymesh/javascript-proxy-headers. README badge sits under the H1, relative notes/license links are absolute, and there are cross-links to python-proxy-headers, ruby-proxy-headers, and scrapy-proxy-headers. jsr.json has no description field (JSR reads that from package settings).

  8. CI (.github/workflows/ci.yml): on pull requests and pushes to main, jobs for unit tests, npm run test:types, and the local mock-proxy suite, each on a Node 22 and 24 matrix. runs-on: ubuntu-24.04 only. .github/workflows/latest-clients.yml runs on a weekly Monday cron and workflow_dispatch: npm ci, then npm install --no-save --no-package-lock of the latest axios, got, ky, make-fetch-happen, needle, node-fetch, superagent, typed-rest-client, undici, and wretch, then unit tests and the local mock-proxy run (including the proxy-traversal assertion). It does not run on pull requests. Existing proxy_integration_tests.yml (real PROXY_URL) is unchanged.

Dependabot

Dependabot auto-merge is unchanged. .github/workflows/dependabot-auto-merge.yml still auto-approves and auto-merges every Dependabot version update, including major devDependency updates, once required checks pass.

Owner settings (not changed here)

Please set these in the GitHub / JSR UI. This PR does not touch repo settings.

  • GitHub description: Send and read custom proxy headers on HTTPS CONNECT for axios, got, node-fetch, undici, ky and more
  • GitHub topics to add: https, connect, tunnel, proxymesh, http-client, typescript
  • JSR package settings (not jsr.json): set the same description (currently empty). Runtime compatibility: only Node is claimed. Do not mark Bun or Deno; this package uses node:http and node:tls and those runtimes were not tested.
  • Optional later: publish from CI with provenance (npx jsr publish, id-token: write) for the JSR provenance score point. Not done in this PR.

Tests run locally (Node v22.23.4)

  • npm run test:unit: 20 tests passed (existing CONNECT security tests plus four traversal tests) on the lockfile and again after installing the latest supported clients.
  • npm run test:types: passed on TypeScript 7.
  • npm run test:local-proxy: 11/11 adapters, and the mock proxy recorded CONNECT plus tunneled bytes, on the lockfile and on latest clients (axios 1.20.0, got 16.1.0, ky 2.2.0, make-fetch-happen 16.0.1, needle 3.5.0, node-fetch 3.3.2, superagent 10.4.1, typed-rest-client 3.1.2, undici 8.12.3, wretch 3.0.9).
  • Undici traversal also passed with undici 7.30.1 swapped in, and with undici 8.12.2 / 8.12.3.
  • ky 1.14.3 still tunnels and returns ProxyResponse; ky 2.2.0 returns a native Response.
  • npm pack style install of this tree plus got@16, undici@8, make-fetch-happen@16, and typed-rest-client@3 did not ERESOLVE.
  • actionlint 1.7.12 reported no findings on the workflows.

Left out on purpose

  • No version bump, tag, or release PR.
  • GitHub description/topics and the JSR description are owner settings.
  • Node still warns DEP0123 when servername is an IP address (ProxyHeadersAgent sets servername to the target host). Pre-existing; not changed.
  • The real-proxy workflow still requires the PROXY_URL secret and is unchanged.
  • The weekly latest-clients workflow is schedule + workflow_dispatch only, so it does not run on this pull request.
Open in Web Open in Cursor 

HTTPS requests passed an open tunnel socket as undici connect.socket, which
the default connector replaces with a direct dial, leaking the client IP.
Use a custom connector on ProxyHeadersAgent and fail tests unless the mock
proxy sees CONNECT and tunneled bytes. Also accept ky 2.2 Responses, widen
peer ranges through the versions we test, require Node >= 22.19, and add
unit, type, and latest-client workflows.

Co-authored-by: ProxyMesh AI <proxymeshai@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants