CWE-180
AllowedIncorrect Behavior Order: Validate Before Canonicalize
Abstraction: Variant · Status: Draft
The product validates input before it is canonicalized, which prevents the product from detecting data that becomes invalid after the canonicalization step.
44 vulnerabilities reference this CWE, most recent first.
GHSA-Q4QF-9J86-F5MH
Vulnerability from github – Published: 2026-04-02 18:44 – Updated: 2026-05-13 15:20Summary
Rack::Static#applicable_rules evaluates several header_rules types against the raw URL-encoded PATH_INFO, while the underlying file-serving path is decoded before the file is served. As a result, a request for a URL-encoded variant of a static path can serve the same file without the headers that header_rules were intended to apply.
In deployments that rely on Rack::Static to attach security-relevant response headers to static content, this can allow an attacker to bypass those headers by requesting an encoded form of the path.
Details
Rack::Static#applicable_rules matches rule types such as :fonts, Array, and Regexp directly against the incoming PATH_INFO. For example:
when :fonts
/\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path)
when Array
/\.(#{rule.join('|')})\z/.match?(path)
when Regexp
rule.match?(path)
These checks operate on the raw request path. If the request contains encoded characters such as %2E in place of ., the rule may fail to match even though the file path is later decoded and served successfully by the static file server.
For example, both of the following requests may resolve to the same file on disk:
/fonts/test.woff
/fonts/test%2Ewoff
but only the unencoded form may receive the headers configured through header_rules.
This creates a canonicalization mismatch between the path used for header policy decisions and the path ultimately used for file serving.
Impact
Applications that rely on Rack::Static header_rules to apply security-relevant headers to static files may be affected.
In affected deployments, an attacker can request an encoded variant of a static file path and receive the same file without the intended headers. Depending on how header_rules are used, this may bypass protections such as clickjacking defenses, content restrictions, or other response policies applied to static content.
The practical impact depends on the configured rules and the types of files being served. If header_rules are only used for non-security purposes such as caching, the issue may have limited security significance.
Mitigation
- Update to a patched version of Rack that applies
header_rulesto a decoded path consistently with static file resolution. - Do not rely solely on
Rack::Staticheader_rulesfor security-critical headers where encoded path variants may reach the application. - Prefer setting security headers at the reverse proxy or web server layer so they apply consistently to both encoded and unencoded path forms.
- Normalize or reject encoded path variants for static content at the edge, where feasible.
{
"affected": [
{
"package": {
"ecosystem": "RubyGems",
"name": "rack"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.2.23"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "RubyGems",
"name": "rack"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0.beta1"
},
{
"fixed": "3.1.21"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "RubyGems",
"name": "rack"
},
"ranges": [
{
"events": [
{
"introduced": "3.2.0"
},
{
"fixed": "3.2.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-34786"
],
"database_specific": {
"cwe_ids": [
"CWE-180"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-02T18:44:49Z",
"nvd_published_at": "2026-04-02T17:16:25Z",
"severity": "MODERATE"
},
"details": "## Summary\n\n`Rack::Static#applicable_rules` evaluates several `header_rules` types against the raw URL-encoded `PATH_INFO`, while the underlying file-serving path is decoded before the file is served. As a result, a request for a URL-encoded variant of a static path can serve the same file without the headers that `header_rules` were intended to apply.\n\nIn deployments that rely on `Rack::Static` to attach security-relevant response headers to static content, this can allow an attacker to bypass those headers by requesting an encoded form of the path.\n\n## Details\n\n`Rack::Static#applicable_rules` matches rule types such as `:fonts`, `Array`, and `Regexp` directly against the incoming `PATH_INFO`. For example:\n\n```ruby\nwhen :fonts\n /\\.(?:ttf|otf|eot|woff2|woff|svg)\\z/.match?(path)\nwhen Array\n /\\.(#{rule.join(\u0027|\u0027)})\\z/.match?(path)\nwhen Regexp\n rule.match?(path)\n```\n\nThese checks operate on the raw request path. If the request contains encoded characters such as `%2E` in place of `.`, the rule may fail to match even though the file path is later decoded and served successfully by the static file server.\n\nFor example, both of the following requests may resolve to the same file on disk:\n\n```text\n/fonts/test.woff\n/fonts/test%2Ewoff\n```\n\nbut only the unencoded form may receive the headers configured through `header_rules`.\n\nThis creates a canonicalization mismatch between the path used for header policy decisions and the path ultimately used for file serving.\n\n## Impact\n\nApplications that rely on `Rack::Static` `header_rules` to apply security-relevant headers to static files may be affected.\n\nIn affected deployments, an attacker can request an encoded variant of a static file path and receive the same file without the intended headers. Depending on how `header_rules` are used, this may bypass protections such as clickjacking defenses, content restrictions, or other response policies applied to static content.\n\nThe practical impact depends on the configured rules and the types of files being served. If `header_rules` are only used for non-security purposes such as caching, the issue may have limited security significance.\n\n## Mitigation\n\n* Update to a patched version of Rack that applies `header_rules` to a decoded path consistently with static file resolution.\n* Do not rely solely on `Rack::Static` `header_rules` for security-critical headers where encoded path variants may reach the application.\n* Prefer setting security headers at the reverse proxy or web server layer so they apply consistently to both encoded and unencoded path forms.\n* Normalize or reject encoded path variants for static content at the edge, where feasible.",
"id": "GHSA-q4qf-9j86-f5mh",
"modified": "2026-05-13T15:20:50Z",
"published": "2026-04-02T18:44:49Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/rack/rack/security/advisories/GHSA-q4qf-9j86-f5mh"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34786"
},
{
"type": "PACKAGE",
"url": "https://github.com/rack/rack"
},
{
"type": "WEB",
"url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/rack/CVE-2026-34786.yml"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Rack:: Static header_rules bypass via URL-encoded paths"
}
GHSA-V2WJ-Q39Q-566R
Vulnerability from github – Published: 2026-04-06 18:03 – Updated: 2026-04-07 22:16Summary
The contents of files that are specified by server.fs.deny can be returned to the browser.
Impact
Only apps that match the following conditions are affected:
- explicitly exposes the Vite dev server to the network (using
--hostorserver.hostconfig option) - the sensitive file exists in the allowed directories specified by
server.fs.allow - the sensitive file is denied with a pattern that matches a file by
server.fs.deny
Details
On the Vite dev server, files that should be blocked by server.fs.deny (e.g., .env, *.crt) can be retrieved with HTTP 200 responses when query parameters such as ?raw, ?import&raw, or ?import&url&inline are appended.
PoC
- Start the dev server:
pnpm exec vite root --host 127.0.0.1 --port 5175 --strictPort - Confirm that
server.fs.denyis enforced (expect 403):curl -i http://127.0.0.1:5175/src/.env | head -n 20 - Confirm that the same files can be retrieved with query parameters (expect 200):
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 8.0.4"
},
"package": {
"ecosystem": "npm",
"name": "vite"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0"
},
{
"fixed": "8.0.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 7.3.1"
},
"package": {
"ecosystem": "npm",
"name": "vite"
},
"ranges": [
{
"events": [
{
"introduced": "7.1.0"
},
{
"fixed": "7.3.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-39364"
],
"database_specific": {
"cwe_ids": [
"CWE-180",
"CWE-284"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-06T18:03:32Z",
"nvd_published_at": "2026-04-07T20:16:30Z",
"severity": "HIGH"
},
"details": "### Summary\n\nThe contents of files that are specified by [`server.fs.deny`](https://vite.dev/config/server-options#server-fs-deny) can be returned to the browser.\n\n### Impact\n\nOnly apps that match the following conditions are affected:\n\n- explicitly exposes the Vite dev server to the network (using `--host` or [`server.host` config option](https://vitejs.dev/config/server-options.html#server-host))\n- the sensitive file exists in the allowed directories specified by [`server.fs.allow`](https://vite.dev/config/server-options#server-fs-allow)\n- the sensitive file is denied with a pattern that matches a file by [`server.fs.deny`](https://vite.dev/config/server-options#server-fs-deny)\n\n### Details\n\nOn the Vite dev server, files that should be blocked by `server.fs.deny` (e.g., `.env`, `*.crt`) can be retrieved with HTTP 200 responses when query parameters such as `?raw`, `?import\u0026raw`, or `?import\u0026url\u0026inline` are appended.\n\n### PoC\n\n1. Start the dev server: `pnpm exec vite root --host 127.0.0.1 --port 5175 --strictPort`\n2. Confirm that `server.fs.deny` is enforced (expect 403): `curl -i http://127.0.0.1:5175/src/.env | head -n 20`\n \u003cimg width=\"3944\" height=\"1092\" alt=\"image\" src=\"https://github.com/user-attachments/assets/ecb9f2e0-e08f-4ac7-b194-e0f988c4cd4f\" /\u003e\n3. Confirm that the same files can be retrieved with query parameters (expect 200):\n \u003cimg width=\"2014\" height=\"373\" alt=\"image\" src=\"https://github.com/user-attachments/assets/76bc2a6a-44f4-4161-ae47-eab5ae0c04a8\" /\u003e",
"id": "GHSA-v2wj-q39q-566r",
"modified": "2026-04-07T22:16:17Z",
"published": "2026-04-06T18:03:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/vitejs/vite/security/advisories/GHSA-v2wj-q39q-566r"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39364"
},
{
"type": "WEB",
"url": "https://github.com/vitejs/vite/pull/22160"
},
{
"type": "WEB",
"url": "https://github.com/vitejs/vite/commit/a9a3df299378d9cbc5f069e3536a369f8188c8ff"
},
{
"type": "PACKAGE",
"url": "https://github.com/vitejs/vite"
},
{
"type": "WEB",
"url": "https://github.com/vitejs/vite/releases/tag/v7.3.2"
},
{
"type": "WEB",
"url": "https://github.com/vitejs/vite/releases/tag/v8.0.5"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Vite: `server.fs.deny` bypassed with queries"
}
GHSA-V5MV-P594-2X33
Vulnerability from github – Published: 2026-08-03 21:07 – Updated: 2026-08-03 21:07Impact
In affected versions, Guzzle gives a transport the request URI as text and supplies the Host header separately. The cURL handlers set CURLOPT_URL to the URI exactly as written and push that Host into CURLOPT_HTTPHEADER; StreamHandler does the same through fopen(). libcurl then parses the authority itself, percent-decoding it and, on an IDN-capable build, applying IDNA mapping, and uses the result to resolve, connect, name the TLS peer and address a proxy CONNECT, while the supplied Host suppresses the aligned one it would have generated. In http://127.0.0.%31/ the URI host is one filter_var() rejects as an IP literal, yet libcurl decodes it to 127.0.0.1 and reaches loopback with no DNS lookup while the server receives Host: 127.0.0.%31.
An attacker who influences a fetched URI can therefore reach a host the application's checks excluded and read whatever it exposes of the response. The same divergence moves Guzzle's own decisions onto a spelling the transport does not use: no_proxy selects proxy routing from the literal host, and RedirectMiddleware decides from it whether to strip Authorization and Cookie. The cookie middleware extracts Set-Cookie against the URI Guzzle produced, not the authority contacted, so for a raw divergent URI the cookie is stored under the URI host as written. Where Guzzle rewrote that URI but left a divergent Host, or where the caller supplied one, the cookie is stored under the canonical name and replayed by ordinary later requests to it. With a third-party UriInterface, a host of blocked.example.com@127.0.0.1 reaches 127.0.0.1 through all three handlers and generates Authorization: Basic from userinfo the application never wrote.
Exploitation requires the application to build a request URI from untrusted input and to make a host decision before handing it to Guzzle. Applications that only fetch URIs they construct themselves are not affected, and an exact allowlist of canonical names ordinarily fails closed; the exposure is to denylists, private-range and IP-literal checks, and any check that treats an unresolvable name as safe. The raw Unicode class needs an IDNA transformation somewhere: either a libcurl built with IDN support or Guzzle's own idn_conversion, off by default on both branches, which rewrites the URI in Client::buildUri() before a handler sees it and leaves a prebuilt request's explicit Host as written, while a request the client builds derives that header from the rewritten URI and produces no divergence. Noncanonical numeric spellings such as 127.1, 2130706433, 0x7f000001 and 0177.0.0.1 remain accepted after the patch and reach whatever the transport reads them as, loopback or a routable public host, and the cURL and stream handlers can differ, so a check comparing a host against an address as text stays bypassable. Guzzle does not offer SSRF protection, and neither cache poisoning nor cross-tenant compromise was established.
Patches
This is a summary; the patches are the authority. The issue is fixed in 7.15.2 and 8.0.1, which validate the request host in all three built-in handlers before any network I/O. A URI host is rejected for a byte outside 0x21 to 0x7E, a percent escape, a URI authority delimiter, unbalanced brackets, or numeric-looking parts followed by a trailing dot. That last rule is deliberately conservative and also refuses out-of-range forms libcurl keeps as names, such as 256.0.0.1.. An explicit Host header must be printable ASCII, and on 7.15.2 free of percent escapes. The client also regenerates a derived Host when it rewrites the request URI. Versions before 7.15.2 and version 8.0.0 are affected.
Workarounds
If you cannot upgrade, constrain the host yourself before handing a URI to Guzzle, and constrain any explicit Host header separately, on every redirect hop. The URI rule assumes $uri is a validated GuzzleHttp\Psr7\Uri, so re-parse a third-party UriInterface with new Uri((string) $uri) first.
$host = $uri->getHost();
if (
preg_match('/\A[\x21-\x7E]*\z/D', $host) !== 1
|| strpbrk($host, '%@/?#\\') !== false
|| substr($host, -1) === '.'
) {
throw new RuntimeException('Refusing to fetch this URI host.');
}
if (
preg_match('/\A[\x21-\x7E]*\z/D', $hostHeader) !== 1
|| strpos($hostHeader, '%') !== false
) {
throw new RuntimeException('Refusing to send this Host header.');
}
It differs from the patch in both directions: it refuses example.com., which the patch accepts, and it does not canonicalize 127.1 or 0x7f000001. Reparsing the URI separates a valid port from the host and rejects malformed bracket forms, so the snippet checks the host component alone. idn_conversion => true is not an access control, since IDNA maps 127。0。0。1 onto 127.0.0.1 and direct handler use bypasses it, and Uri::getHost() is not an SSRF boundary: it is the host as written, not the host a transport connects to. Where the destination matters, resolve the host and check the addresses, and use a separate cookie jar for untrusted origins.
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "guzzlehttp/guzzle"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "7.15.2"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "guzzlehttp/guzzle"
},
"ranges": [
{
"events": [
{
"introduced": "8.0.0"
},
{
"fixed": "8.0.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-69246"
],
"database_specific": {
"cwe_ids": [
"CWE-180",
"CWE-436",
"CWE-918",
"CWE-941"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-03T21:07:26Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Impact\n\nIn affected versions, Guzzle gives a transport the request URI as text and supplies the `Host` header separately. The cURL handlers set `CURLOPT_URL` to the URI exactly as written and push that `Host` into `CURLOPT_HTTPHEADER`; `StreamHandler` does the same through `fopen()`. libcurl then parses the authority itself, percent-decoding it and, on an IDN-capable build, applying IDNA mapping, and uses the result to resolve, connect, name the TLS peer and address a proxy `CONNECT`, while the supplied `Host` suppresses the aligned one it would have generated. In `http://127.0.0.%31/` the URI host is one `filter_var()` rejects as an IP literal, yet libcurl decodes it to `127.0.0.1` and reaches loopback with no DNS lookup while the server receives `Host: 127.0.0.%31`.\n\nAn attacker who influences a fetched URI can therefore reach a host the application\u0027s checks excluded and read whatever it exposes of the response. The same divergence moves Guzzle\u0027s own decisions onto a spelling the transport does not use: `no_proxy` selects proxy routing from the literal host, and `RedirectMiddleware` decides from it whether to strip `Authorization` and `Cookie`. The cookie middleware extracts `Set-Cookie` against the URI Guzzle produced, not the authority contacted, so for a raw divergent URI the cookie is stored under the URI host as written. Where Guzzle rewrote that URI but left a divergent `Host`, or where the caller supplied one, the cookie is stored under the canonical name and replayed by ordinary later requests to it. With a third-party `UriInterface`, a host of `blocked.example.com@127.0.0.1` reaches `127.0.0.1` through all three handlers and generates `Authorization: Basic` from userinfo the application never wrote.\n\nExploitation requires the application to build a request URI from untrusted input and to make a host decision before handing it to Guzzle. Applications that only fetch URIs they construct themselves are not affected, and an exact allowlist of canonical names ordinarily fails closed; the exposure is to denylists, private-range and IP-literal checks, and any check that treats an unresolvable name as safe. The raw Unicode class needs an IDNA transformation somewhere: either a libcurl built with IDN support or Guzzle\u0027s own `idn_conversion`, off by default on both branches, which rewrites the URI in `Client::buildUri()` before a handler sees it and leaves a prebuilt request\u0027s explicit `Host` as written, while a request the client builds derives that header from the rewritten URI and produces no divergence. Noncanonical numeric spellings such as `127.1`, `2130706433`, `0x7f000001` and `0177.0.0.1` remain accepted after the patch and reach whatever the transport reads them as, loopback or a routable public host, and the cURL and stream handlers can differ, so a check comparing a host against an address as text stays bypassable. Guzzle does not offer SSRF protection, and neither cache poisoning nor cross-tenant compromise was established.\n\n### Patches\n\nThis is a summary; the patches are the authority. The issue is fixed in `7.15.2` and `8.0.1`, which validate the request host in all three built-in handlers before any network I/O. A URI host is rejected for a byte outside `0x21` to `0x7E`, a percent escape, a URI authority delimiter, unbalanced brackets, or numeric-looking parts followed by a trailing dot. That last rule is deliberately conservative and also refuses out-of-range forms libcurl keeps as names, such as `256.0.0.1.`. An explicit `Host` header must be printable ASCII, and on `7.15.2` free of percent escapes. The client also regenerates a derived `Host` when it rewrites the request URI. Versions before `7.15.2` and version `8.0.0` are affected.\n\n### Workarounds\n\nIf you cannot upgrade, constrain the host yourself before handing a URI to Guzzle, and constrain any explicit `Host` header separately, on every redirect hop. The URI rule assumes `$uri` is a validated `GuzzleHttp\\Psr7\\Uri`, so re-parse a third-party `UriInterface` with `new Uri((string) $uri)` first.\n\n```php\n$host = $uri-\u003egetHost();\n\nif (\n preg_match(\u0027/\\A[\\x21-\\x7E]*\\z/D\u0027, $host) !== 1\n || strpbrk($host, \u0027%@/?#\\\\\u0027) !== false\n || substr($host, -1) === \u0027.\u0027\n) {\n throw new RuntimeException(\u0027Refusing to fetch this URI host.\u0027);\n}\n\nif (\n preg_match(\u0027/\\A[\\x21-\\x7E]*\\z/D\u0027, $hostHeader) !== 1\n || strpos($hostHeader, \u0027%\u0027) !== false\n) {\n throw new RuntimeException(\u0027Refusing to send this Host header.\u0027);\n}\n```\n\nIt differs from the patch in both directions: it refuses `example.com.`, which the patch accepts, and it does not canonicalize `127.1` or `0x7f000001`. Reparsing the URI separates a valid port from the host and rejects malformed bracket forms, so the snippet checks the host component alone. `idn_conversion =\u003e true` is not an access control, since IDNA maps `\uff11\uff12\uff17\u3002\uff10\u3002\uff10\u3002\uff11` onto `127.0.0.1` and direct handler use bypasses it, and `Uri::getHost()` is not an SSRF boundary: it is the host as written, not the host a transport connects to. Where the destination matters, resolve the host and check the addresses, and use a separate cookie jar for untrusted origins.",
"id": "GHSA-v5mv-p594-2x33",
"modified": "2026-08-03T21:07:26Z",
"published": "2026-08-03T21:07:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/guzzle/guzzle/security/advisories/GHSA-v5mv-p594-2x33"
},
{
"type": "WEB",
"url": "https://github.com/guzzle/guzzle/pull/3907"
},
{
"type": "WEB",
"url": "https://github.com/guzzle/guzzle/pull/3908"
},
{
"type": "WEB",
"url": "https://github.com/guzzle/guzzle/commit/3aeea0406aab88cbbd86531313d7cebf8ae149a4"
},
{
"type": "WEB",
"url": "https://github.com/guzzle/guzzle/commit/744101956d78b7c1384d0cbf379db13e859167bf"
},
{
"type": "PACKAGE",
"url": "https://github.com/guzzle/guzzle"
},
{
"type": "WEB",
"url": "https://github.com/guzzle/guzzle/releases/tag/7.15.2"
},
{
"type": "WEB",
"url": "https://github.com/guzzle/guzzle/releases/tag/8.0.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Guzzle: Noncanonical host can bypass host-based checks"
}
GHSA-XPCF-PG52-R92G
Vulnerability from github – Published: 2026-04-08 00:17 – Updated: 2026-04-24 21:06Summary
ipRestriction() does not canonicalize IPv4-mapped IPv6 client addresses (e.g. ::ffff:127.0.0.1) before applying IPv4 allow or deny rules. In environments such as Node.js dual-stack, this can cause IPv4 rules to fail to match, leading to unintended authorization behavior.
Details
The middleware classifies client addresses based on their textual form. Addresses containing ":" are treated as IPv6, including IPv4-mapped IPv6 addresses such as ::ffff:127.0.0.1. These addresses are not normalized to IPv4 before matching.
As a result:
- IPv4 static rules (e.g.
127.0.0.1) do not match because the raw string differs - IPv4 CIDR rules (e.g.
127.0.0.0/8,10.0.0.0/8) are skipped because the address is treated as IPv6
For example, with:
denyList: ['127.0.0.1']
a request from 127.0.0.1 may be represented as ::ffff:127.0.0.1 and bypass the deny rule.
This behavior commonly occurs in Node.js environments where IPv4 clients are exposed as IPv4-mapped IPv6 addresses.
Impact
Applications that rely on IPv4-based ipRestriction() rules may incorrectly allow or deny requests.
In affected deployments, a denied IPv4 client may bypass access restrictions. Conversely, legitimate clients may be rejected when using IPv4 allow lists.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "hono"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.12.12"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-39409"
],
"database_specific": {
"cwe_ids": [
"CWE-180"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-08T00:17:14Z",
"nvd_published_at": "2026-04-08T15:16:14Z",
"severity": "MODERATE"
},
"details": "## Summary\n\n`ipRestriction()` does not canonicalize IPv4-mapped IPv6 client addresses (e.g. `::ffff:127.0.0.1`) before applying IPv4 allow or deny rules. In environments such as Node.js dual-stack, this can cause IPv4 rules to fail to match, leading to unintended authorization behavior.\n\n## Details\n\nThe middleware classifies client addresses based on their textual form. Addresses containing \"`:`\" are treated as IPv6, including IPv4-mapped IPv6 addresses such as `::ffff:127.0.0.1`. These addresses are not normalized to IPv4 before matching.\n\nAs a result:\n\n* IPv4 static rules (e.g. `127.0.0.1`) do not match because the raw string differs\n* IPv4 CIDR rules (e.g. `127.0.0.0/8`, `10.0.0.0/8`) are skipped because the address is treated as IPv6\n\nFor example, with:\n\n`denyList: [\u0027127.0.0.1\u0027]`\n\na request from `127.0.0.1` may be represented as `::ffff:127.0.0.1` and bypass the deny rule.\n\nThis behavior commonly occurs in Node.js environments where IPv4 clients are exposed as IPv4-mapped IPv6 addresses.\n\n## Impact\n\nApplications that rely on IPv4-based `ipRestriction()` rules may incorrectly allow or deny requests.\n\nIn affected deployments, a denied IPv4 client may bypass access restrictions. Conversely, legitimate clients may be rejected when using IPv4 allow lists.",
"id": "GHSA-xpcf-pg52-r92g",
"modified": "2026-04-24T21:06:13Z",
"published": "2026-04-08T00:17:14Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/honojs/hono/security/advisories/GHSA-xpcf-pg52-r92g"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39409"
},
{
"type": "WEB",
"url": "https://github.com/honojs/hono/commit/48fa2233bc092f650119f42df043050737cabf39"
},
{
"type": "PACKAGE",
"url": "https://github.com/honojs/hono"
},
{
"type": "WEB",
"url": "https://github.com/honojs/hono/releases/tag/v4.12.12"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
},
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Hono has incorrect IP matching in ipRestriction() for IPv4-mapped IPv6 addresses"
}
Mitigation MIT-20
Strategy: Input Validation
Inputs should be decoded and canonicalized to the application's current internal representation before being validated (CWE-180). Make sure that the application does not decode the same input twice (CWE-174). Such errors could be used to bypass allowlist validation schemes by introducing dangerous inputs after they have been checked.
CAPEC-267: Leverage Alternate Encoding
An adversary leverages the possibility to encode potentially harmful input or content used by applications such that the applications are ineffective at validating this encoding standard.
CAPEC-3: Using Leading 'Ghost' Character Sequences to Bypass Input Filters
Some APIs will strip certain leading characters from a string of parameters. An adversary can intentionally introduce leading "ghost" characters (extra characters that don't affect the validity of the request at the API layer) that enable the input to pass the filters and therefore process the adversary's input. This occurs when the targeted API will accept input data in several syntactic forms and interpret it in the equivalent semantic way, while the filter does not take into account the full spectrum of the syntactic forms acceptable to the targeted API.
CAPEC-71: Using Unicode Encoding to Bypass Validation Logic
An attacker may provide a Unicode string to a system component that is not Unicode aware and use that to circumvent the filter or cause the classifying mechanism to fail to properly understanding the request. That may allow the attacker to slip malicious data past the content filter and/or possibly cause the application to route the request incorrectly.
CAPEC-78: Using Escaped Slashes in Alternate Encoding
This attack targets the use of the backslash in alternate encoding. An adversary can provide a backslash as a leading character and causes a parser to believe that the next character is special. This is called an escape. By using that trick, the adversary tries to exploit alternate ways to encode the same character which leads to filter problems and opens avenues to attack.
CAPEC-79: Using Slashes in Alternate Encoding
This attack targets the encoding of the Slash characters. An adversary would try to exploit common filtering problems related to the use of the slashes characters to gain access to resources on the target host. Directory-driven systems, such as file systems and databases, typically use the slash character to indicate traversal between directories or other container components. For murky historical reasons, PCs (and, as a result, Microsoft OSs) choose to use a backslash, whereas the UNIX world typically makes use of the forward slash. The schizophrenic result is that many MS-based systems are required to understand both forms of the slash. This gives the adversary many opportunities to discover and abuse a number of common filtering problems. The goal of this pattern is to discover server software that only applies filters to one version, but not the other.
CAPEC-80: Using UTF-8 Encoding to Bypass Validation Logic
This attack is a specific variation on leveraging alternate encodings to bypass validation logic. This attack leverages the possibility to encode potentially harmful input in UTF-8 and submit it to applications not expecting or effective at validating this encoding standard making input filtering difficult. UTF-8 (8-bit UCS/Unicode Transformation Format) is a variable-length character encoding for Unicode. Legal UTF-8 characters are one to four bytes long. However, early version of the UTF-8 specification got some entries wrong (in some cases it permitted overlong characters). UTF-8 encoders are supposed to use the "shortest possible" encoding, but naive decoders may accept encodings that are longer than necessary. According to the RFC 3629, a particularly subtle form of this attack can be carried out against a parser which performs security-critical validity checks against the UTF-8 encoded form of its input, but interprets certain illegal octet sequences as characters.