GHSA-MGHH-PGCX-3JJJ

Vulnerability from github – Published: 2026-09-30 15:03 – Updated: 2026-09-30 15:03
VLAI
Summary
Axios: ReDoS (O(N²)) in shouldBypassProxy host normalization, reachable via untrusted redirect Location
Details

Summary

Axios shouldBypassProxy() normalizes hostnames with hostname.replace(/\.+$/, ''). For a hostname shaped as many dots followed by a non-dot, the anchored regex can perform quadratic backtracking. Because axios re-evaluates proxy bypass rules for redirected requests, a malicious server can trigger this synchronous work through a crafted redirect Location.

The issue affects Node.js applications that use environment proxy variables with NO_PROXY and allow redirects.

Impact

An attacker-controlled server can return a redirect whose hostname causes the axios process to spend significant CPU time in synchronous hostname normalization. During this time, the Node.js event loop is blocked and the application cannot handle other work on that thread.

This is an availability-only issue. It does not disclose request data or modify requests.

Affected Functionality

Affected:

  • Node.js HTTP adapter.
  • Environment proxy handling through HTTP_PROXY or HTTPS_PROXY.
  • NO_PROXY or no_proxy set to a non-empty value.
  • Redirects followed by axios or follow-redirects.

Not affected:

  • Browser adapters.
  • Requests with proxy: false.
  • Requests with no NO_PROXY value.
  • Requests with maxRedirects: 0, unless application code manually follows the malicious redirect and re-enters axios.

Technical Details

lib/helpers/shouldBypassProxy.js contains:

return unmapIPv4MappedIPv6(hostname.replace(/\.+$/, ''));

When the hostname is "." * n + "a", the $ anchor causes the regex engine to retry the dot run from many positions before it fails. setProxy() invokes shouldBypassProxy(location) after getProxyForUrl(location) returns a proxy, including on redirect hops via beforeRedirects.proxy.

Local timing on axios 1.18.1 showed increasing cost for crafted hostnames: about 1 ms at 1000 dots, 6.9 ms at 3000 dots, and 34.5 ms at 6000 dots. The growth is consistent with the submitted quadratic claim while avoiding long-running payloads.

Proof of Concept of Attack

Constrained helper-level demonstration:

import shouldBypassProxy from 'axios/lib/helpers/shouldBypassProxy.js';

process.env.NO_PROXY = 'example.com';
shouldBypassProxy('http://' + '.'.repeat(6000) + 'a/');

In the full adapter path, a malicious server can return that hostname in a 302 Location header while the client has proxy environment variables and NO_PROXY configured.

Workarounds

Disable automatic redirects for requests to untrusted servers, or avoid environment proxy handling for those requests with proxy: false when appropriate. Operators can also avoid broad untrusted redirect-following in services where event-loop availability is critical.

Original report

### Summary shouldBypassProxy normalizes a host with hostname.replace(/\.+$/, ''). On a host of the shape (e.g. "." × 40000 + "a"), this anchored regex backtracks quadratically (O(N²)), synchronously starving the Node.js event loop. Because axios re-evaluates the proxy on every redirect using the new Location host, a malicious server can return a crafted 302 Location and freeze the client's event loop for seconds per redirect — a denial of service. ### Details - Affected code: lib/helpers/shouldBypassProxy.js → normalizeNoProxyHost: hostname.replace(/\.+$/, '') (one pass in 1.18.1). The open PR #11029 adds a second /\.+$/ pass in normalizeIPAddress, doubling the cost (not the origin). - Root cause: /\.+$/ is O(N²) on a long run of dots that is not at the end of the string — the $ anchor forces the engine to backtrack the entire dot-run from every start position. - Trust boundary (per THREATMODEL): the redirect Location is untrusted (T-2: "axios to network … redirect Location … untrusted"). The caller requests a benign URL; the malicious host arrives via the server's 302. This is not the T-1 caller-supplied-URL non-goal. - Call chain: setProxy(options, configProxy, location, isRedirect, …) (lib/adapters/http.js) → env-proxy branch → getProxyForUrl(location) returns a proxy → if (!shouldBypassProxy(location)) → normalizeNoProxyHost(parsed.hostname.toLowerCase()) runs /\.+$/. setProxy is re-invoked on the redirect hop with the untrusted Location; new URL() retains the long dot-run in .hostname. - Preconditions: proxy configured via environment (HTTP_PROXY/HTTPS_PROXY, trusted per THREATMODEL T-3, common in CI/containers/enterprise) + NO_PROXY set + redirects followed (default maxRedirects: 5). ### PoC Self-contained, no network — imports axios's own helper: // node poc.mjs (run next to an axios install) import sbp from './node_modules/axios/lib/helpers/shouldBypassProxy.js'; process.env.NO_PROXY = 'example.com'; for (const n of [5000, 10000, 20000, 40000]) { const url = 'http://' + '.'.repeat(n) + 'a/'; // arrives as an untrusted 302 Location host const t0 = process.hrtime.bigint(); sbp(url); // returns false (correct no-bypass) — but O(N^2) slow console.log(`N=${n}: ${(Number(process.hrtime.bigint()-t0)/1e6)|0} ms`); } // Measured on 1.18.1: N=5000 ~43ms, 10000 ~160ms, 20000 ~618ms, 40000 ~2499ms; benign host ~0.05ms. Driven through the real http-adapter __setProxy on the redirect path (setProxy(…, isRedirect=true)), a 10 ms timer fires 0 times during the ~2.4 s block at N=40000 — full event-loop starvation. ### Impact Denial of service (event-loop starvation) on any axios client that uses an environment proxy with NO_PROXY set and follows redirects, when a server it contacts returns a crafted redirect Location. No data exposure or RCE. Same impact class as the accepted DoS advisories GHSA-62hf-57xw-28j9 (toFormData recursion) and the maxContentLength response-size DoS. ### Suggested fix Replace the regex trailing-dot strip with a linear trim: let end = hostname.length; while (end > 0 && hostname.charCodeAt(end - 1) === 46 /* '.' */) end--; hostname = hostname.slice(0, end); Also drop the redundant second /\.+$/ pass in PR #11029's normalizeIPAddress.
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "axios"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.15.0"
            },
            {
              "fixed": "1.20.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-101906"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-1333",
      "CWE-400"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-30T15:03:02Z",
    "nvd_published_at": "2026-09-28T18:17:19Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nAxios `shouldBypassProxy()` normalizes hostnames with `hostname.replace(/\\.+$/, \u0027\u0027)`. For a hostname shaped as many dots followed by a non-dot, the anchored regex can perform quadratic backtracking. Because axios re-evaluates proxy bypass rules for redirected requests, a malicious server can trigger this synchronous work through a crafted redirect `Location`.\n\nThe issue affects Node.js applications that use environment proxy variables with `NO_PROXY` and allow redirects.\n\n## Impact\n\nAn attacker-controlled server can return a redirect whose hostname causes the axios process to spend significant CPU time in synchronous hostname normalization. During this time, the Node.js event loop is blocked and the application cannot handle other work on that thread.\n\nThis is an availability-only issue. It does not disclose request data or modify requests.\n\n## Affected Functionality\n\nAffected:\n\n- Node.js HTTP adapter.\n- Environment proxy handling through `HTTP_PROXY` or `HTTPS_PROXY`.\n- `NO_PROXY` or `no_proxy` set to a non-empty value.\n- Redirects followed by axios or `follow-redirects`.\n\nNot affected:\n\n- Browser adapters.\n- Requests with `proxy: false`.\n- Requests with no `NO_PROXY` value.\n- Requests with `maxRedirects: 0`, unless application code manually follows the malicious redirect and re-enters axios.\n\n## Technical Details\n\n`lib/helpers/shouldBypassProxy.js` contains:\n\n```js\nreturn unmapIPv4MappedIPv6(hostname.replace(/\\.+$/, \u0027\u0027));\n```\n\nWhen the hostname is `\".\" * n + \"a\"`, the `$` anchor causes the regex engine to retry the dot run from many positions before it fails. `setProxy()` invokes `shouldBypassProxy(location)` after `getProxyForUrl(location)` returns a proxy, including on redirect hops via `beforeRedirects.proxy`.\n\nLocal timing on axios `1.18.1` showed increasing cost for crafted hostnames: about 1 ms at 1000 dots, 6.9 ms at 3000 dots, and 34.5 ms at 6000 dots. The growth is consistent with the submitted quadratic claim while avoiding long-running payloads.\n\n## Proof of Concept of Attack\n\nConstrained helper-level demonstration:\n\n```js\nimport shouldBypassProxy from \u0027axios/lib/helpers/shouldBypassProxy.js\u0027;\n\nprocess.env.NO_PROXY = \u0027example.com\u0027;\nshouldBypassProxy(\u0027http://\u0027 + \u0027.\u0027.repeat(6000) + \u0027a/\u0027);\n```\n\nIn the full adapter path, a malicious server can return that hostname in a `302 Location` header while the client has proxy environment variables and `NO_PROXY` configured.\n\n## Workarounds\n\nDisable automatic redirects for requests to untrusted servers, or avoid environment proxy handling for those requests with `proxy: false` when appropriate. Operators can also avoid broad untrusted redirect-following in services where event-loop availability is critical.\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003ch3\u003eOriginal report\u003c/h3\u003e\u003c/summary\u003e\n  \n### Summary\n\nshouldBypassProxy normalizes a host with hostname.replace(/\\.+$/, \u0027\u0027). On a host of the shape \u003cmany dots\u003e\u003cnon-dot\u003e (e.g. \".\" \u00d7 40000 + \"a\"), this anchored regex backtracks quadratically (O(N\u00b2)), synchronously starving the Node.js event loop. Because axios re-evaluates the proxy on every redirect using the new Location host, a malicious server can return a crafted 302 Location and freeze the client\u0027s event loop for seconds per redirect \u2014 a denial of service.\n\n### Details\n\n- Affected code: lib/helpers/shouldBypassProxy.js \u2192 normalizeNoProxyHost: hostname.replace(/\\.+$/, \u0027\u0027) (one pass in 1.18.1). The open PR #11029 adds a second /\\.+$/ pass in normalizeIPAddress, doubling the cost (not the origin).\n- Root cause: /\\.+$/ is O(N\u00b2) on a long run of dots that is not at the end of the string \u2014 the $ anchor forces the engine to backtrack the entire dot-run from every start position.\n- Trust boundary (per THREATMODEL): the redirect Location is untrusted (T-2: \"axios to network \u2026 redirect Location \u2026 untrusted\"). The caller requests a benign URL; the malicious host arrives via the server\u0027s\u00a0302. This is not the T-1 caller-supplied-URL non-goal.\n- Call chain: setProxy(options, configProxy, location, isRedirect, \u2026) (lib/adapters/http.js) \u2192 env-proxy branch \u2192 getProxyForUrl(location) returns a proxy \u2192 if (!shouldBypassProxy(location)) \u2192 normalizeNoProxyHost(parsed.hostname.toLowerCase()) runs /\\.+$/. setProxy is re-invoked on the redirect hop with the untrusted Location; new URL() retains the long dot-run in .hostname.\n- Preconditions: proxy configured via environment (HTTP_PROXY/HTTPS_PROXY, trusted per THREATMODEL T-3, common in CI/containers/enterprise) + NO_PROXY set + redirects followed (default maxRedirects: 5).\n\n### PoC\n\nSelf-contained, no network \u2014 imports axios\u0027s own helper:\n// node poc.mjs   (run next to an axios install)\nimport sbp from \u0027./node_modules/axios/lib/helpers/shouldBypassProxy.js\u0027;\nprocess.env.NO_PROXY = \u0027example.com\u0027;\nfor (const n of [5000, 10000, 20000, 40000]) {\n  const url = \u0027http://\u0027 + \u0027.\u0027.repeat(n) + \u0027a/\u0027;   // arrives as an untrusted 302 Location host\n  const t0 = process.hrtime.bigint();\n  sbp(url);                                         // returns false (correct no-bypass) \u2014 but O(N^2) slow\n  console.log(`N=${n}: ${(Number(process.hrtime.bigint()-t0)/1e6)|0} ms`);\n}\n// Measured on 1.18.1: N=5000 ~43ms, 10000 ~160ms, 20000 ~618ms, 40000 ~2499ms; benign host ~0.05ms.\nDriven through the real http-adapter __setProxy on the redirect path (setProxy(\u2026, isRedirect=true)), a 10 ms timer fires 0 times during the ~2.4 s block at N=40000 \u2014 full event-loop starvation.\n\n### Impact\n\nDenial of service (event-loop starvation) on any axios client that uses an environment proxy with NO_PROXY set and follows redirects, when a server it contacts returns a crafted redirect Location. No data exposure or RCE. Same impact class as the accepted DoS advisories GHSA-62hf-57xw-28j9 (toFormData recursion) and the maxContentLength response-size DoS.\n\n### Suggested fix\n\nReplace the regex trailing-dot strip with a linear trim:\nlet end = hostname.length;\nwhile (end \u003e 0 \u0026\u0026 hostname.charCodeAt(end - 1) === 46 /* \u0027.\u0027 */) end--;\nhostname = hostname.slice(0, end);\nAlso drop the redundant second /\\.+$/ pass in PR #11029\u0027s normalizeIPAddress.\n\u003c/details\u003e\n\n---",
  "id": "GHSA-mghh-pgcx-3jjj",
  "modified": "2026-09-30T15:03:02Z",
  "published": "2026-09-30T15:03:02Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/axios/axios/security/advisories/GHSA-mghh-pgcx-3jjj"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-101906"
    },
    {
      "type": "WEB",
      "url": "https://github.com/axios/axios/pull/11141"
    },
    {
      "type": "WEB",
      "url": "https://github.com/axios/axios/commit/d19040bda7a8be2f82c3c6e1a5bc03917daee39a"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/axios/axios"
    },
    {
      "type": "WEB",
      "url": "https://github.com/axios/axios/releases/tag/v1.20.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Axios: ReDoS (O(N\u00b2)) in shouldBypassProxy host normalization, reachable via untrusted redirect Location"
}



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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…