Medium severity5.3NVD Advisory· Published Apr 8, 2026· Updated Apr 21, 2026
CVE-2026-39409
CVE-2026-39409
Description
Hono is a Web application framework that provides support for any JavaScript runtime. Prior to 4.12.12, 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. This vulnerability is fixed in 4.12.12.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
hononpm | < 4.12.12 | 4.12.12 |
Affected products
1Patches
13 files changed · +83 −15
src/middleware/ip-restriction/index.test.ts+26 −0 modified@@ -96,6 +96,24 @@ describe('isMatchForRule', () => { expect(await isMatch({ addr: '192.168.2.1', type: 'IPv4' }, '192.168.2.2/32')).toBeFalsy() expect(await isMatch({ addr: '::0', type: 'IPv6' }, '::0/1')).toBeTruthy() + expect(await isMatch({ addr: '::1', type: 'IPv6' }, '0.0.0.0/24')).toBeFalsy() + expect(await isMatch({ addr: '::abcd:1', type: 'IPv6' }, '127.0.0.0/8')).toBeFalsy() + expect(await isMatch({ addr: '::1', type: 'IPv6' }, '::ffff:1.0.0.0/96')).toBeFalsy() + + expect( + await isMatch({ addr: '::ffff:192.168.1.1', type: 'IPv6' }, '::ffff:192.168.1.0/120') + ).toBeTruthy() + expect( + await isMatch({ addr: '192.168.1.1', type: 'IPv4' }, '::ffff:192.168.1.0/120') + ).toBeTruthy() + expect( + await isMatch({ addr: '::ffff:192.168.1.1', type: 'IPv6' }, '192.168.1.0/24') + ).toBeTruthy() + expect(await isMatch({ addr: '::ffff:10.0.0.1', type: 'IPv6' }, '192.168.1.0/24')).toBeFalsy() + expect(await isMatch({ addr: '::ffff:192.168.1.1', type: 'IPv6' }, '::/0')).toBeTruthy() + expect( + await isMatch({ addr: '::ffff:192.168.1.1', type: 'IPv6' }, '::ffff:0:0/95') + ).toBeTruthy() }) it('Static Rules', async () => { expect(await isMatch({ addr: '192.168.2.1', type: 'IPv4' }, '192.168.2.1')).toBeTruthy() @@ -104,6 +122,14 @@ describe('isMatchForRule', () => { await isMatch({ addr: '::ffff:127.0.0.1', type: 'IPv6' }, '::ffff:127.0.0.1') ).toBeTruthy() expect(await isMatch({ addr: '::ffff:127.0.0.1', type: 'IPv6' }, '::ffff:7f00:1')).toBeTruthy() + expect(await isMatch({ addr: '127.0.0.1', type: 'IPv4' }, '::ffff:127.0.0.1')).toBeTruthy() + // IPv4-mapped IPv6 addresses are canonicalized to IPv4 + expect(await isMatch({ addr: '::ffff:127.0.0.1', type: 'IPv6' }, '127.0.0.1')).toBeTruthy() + expect(await isMatch({ addr: '::ffff:127.0.0.1', type: 'IPv6' }, '127.0.0.2')).toBeFalsy() + // non-dotted IPv4-mapped forms are not canonicalized (treated as IPv6) + expect(await isMatch({ addr: '::ffff:7f00:1', type: 'IPv6' }, '127.0.0.1')).toBeFalsy() + // regular IPv6 is not affected + expect(await isMatch({ addr: '::1', type: 'IPv6' }, '::1')).toBeTruthy() }) it('Function Rules', async () => { expect(await isMatch({ addr: '0.0.0.0', type: 'IPv4' }, () => true)).toBeTruthy()
src/middleware/ip-restriction/index.ts+41 −12 modified@@ -7,10 +7,12 @@ import type { Context, MiddlewareHandler } from '../..' import type { AddressType, GetConnInfo } from '../../helper/conninfo' import { HTTPException } from '../../http-exception' import { + convertIPv4MappedIPv6ToIPv4, convertIPv4ToBinary, convertIPv6BinaryToString, convertIPv6ToBinary, distinctRemoteAddr, + isIPv4MappedIPv6, } from '../../utils/ipaddr' /** @@ -56,14 +58,20 @@ const buildMatcher = ( throw new TypeError(`Invalid rule: ${rule}`) } - const isIPv4 = type === 'IPv4' - const prefix = parseInt(separatedRule[1]) + let isIPv4 = type === 'IPv4' + let prefix = parseInt(separatedRule[1]) if (isIPv4 ? prefix === 32 : prefix === 128) { // this rule is a static rule rule = addrStr } else { - const addr = (isIPv4 ? convertIPv4ToBinary : convertIPv6ToBinary)(addrStr) + let addr = (isIPv4 ? convertIPv4ToBinary : convertIPv6ToBinary)(addrStr) + if (type === 'IPv6' && isIPv4MappedIPv6(addr) && prefix >= 96) { + isIPv4 = true + addr = convertIPv4MappedIPv6ToIPv4(addr) + prefix -= 96 + } + const mask = ((1n << BigInt(prefix)) - 1n) << BigInt((isIPv4 ? 32 : 128) - prefix) cidrRules.push([isIPv4, addr & mask, mask] as [boolean, bigint, bigint]) @@ -75,11 +83,17 @@ const buildMatcher = ( if (type === undefined) { throw new TypeError(`Invalid rule: ${rule}`) } - staticRules.add( - type === 'IPv4' - ? rule // IPv4 address is already normalized, so it is registered as is. - : convertIPv6BinaryToString(convertIPv6ToBinary(rule)) // normalize IPv6 address (e.g. 0000:0000:0000:0000:0000:0000:0000:0001 => ::1) - ) + if (type === 'IPv4') { + staticRules.add(rule) + staticRules.add(`::ffff:${rule}`) + } else { + const ipv6binary = convertIPv6ToBinary(rule) + const ipv6Addr = convertIPv6BinaryToString(ipv6binary) + staticRules.add(ipv6Addr) + if (isIPv4MappedIPv6(ipv6binary)) { + staticRules.add(ipv6Addr.substring(7)) // remove ::ffff: prefix + } + } } } @@ -92,13 +106,28 @@ const buildMatcher = ( if (staticRules.has(remote.addr)) { return true } + const remoteAddr = (remote.binaryAddr ||= ( + remote.isIPv4 ? convertIPv4ToBinary : convertIPv6ToBinary + )(remote.addr)) + const remoteIPv4Addr = + remote.isIPv4 || isIPv4MappedIPv6(remoteAddr) + ? remote.isIPv4 + ? remoteAddr + : convertIPv4MappedIPv6ToIPv4(remoteAddr) + : undefined for (const [isIPv4, addr, mask] of cidrRules) { - if (isIPv4 !== remote.isIPv4) { + if (isIPv4) { + if (remoteIPv4Addr === undefined) { + continue + } + if ((remoteIPv4Addr & mask) === addr) { + return true + } + continue + } + if (remote.isIPv4) { continue } - const remoteAddr = (remote.binaryAddr ||= ( - isIPv4 ? convertIPv4ToBinary : convertIPv6ToBinary - )(remote.addr)) if ((remoteAddr & mask) === addr) { return true }
src/utils/ipaddr.ts+16 −3 modified@@ -93,15 +93,28 @@ export const convertIPv4BinaryToString = (ipV4: bigint): string => { return sections.join('.') } +/** + * Check if a binary IPv6 address is an IPv4-mapped IPv6 address (::ffff:x.x.x.x) + * @param ipv6binary binary IPv6 Address + * @return true if the address is an IPv4-mapped IPv6 address + */ +export const isIPv4MappedIPv6 = (ipv6binary: bigint): boolean => ipv6binary >> 32n === 0xffffn + +/** + * Extract the IPv4 portion from an IPv4-mapped IPv6 address + * @param ipv6binary binary IPv4-mapped IPv6 Address + * @return binary IPv4 Address + */ +export const convertIPv4MappedIPv6ToIPv4 = (ipv6binary: bigint): bigint => ipv6binary & 0xffffffffn + /** * Convert a binary representation of an IPv6 address to a string. * @param ipV6 binary IPv6 Address * @return normalized IPv6 Address in string */ export const convertIPv6BinaryToString = (ipV6: bigint): string => { - // IPv6-mapped IPv4 address - if (ipV6 >> 32n === 0xffffn) { - return `::ffff:${convertIPv4BinaryToString(ipV6 & 0xffffffffn)}` + if (isIPv4MappedIPv6(ipV6)) { + return `::ffff:${convertIPv4BinaryToString(convertIPv4MappedIPv6ToIPv4(ipV6))}` } const sections = []
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/honojs/hono/commit/48fa2233bc092f650119f42df043050737cabf39nvdPatchWEB
- github.com/advisories/GHSA-xpcf-pg52-r92gghsaADVISORY
- github.com/honojs/hono/security/advisories/GHSA-xpcf-pg52-r92gnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-39409ghsaADVISORY
- github.com/honojs/hono/releases/tag/v4.12.12nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.