VYPR
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.

PackageAffected versionsPatched versions
hononpm
< 4.12.124.12.12

Affected products

1
  • cpe:2.3:a:hono:hono:*:*:*:*:*:node.js:*:*
    Range: <=4.12.11

Patches

1
48fa2233bc09

Merge commit from fork

https://github.com/honojs/honoYusuke WadaApr 7, 2026via ghsa
3 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

News mentions

0

No linked articles in our index yet.