This is an automated email from the ASF dual-hosted git repository.

hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin-node-casbin.git


The following commit(s) were added to refs/heads/master by this push:
     new 72d0563  chore: fix ipMatch code
72d0563 is described below

commit 72d05635a0400deb7c0f0328ae31ff985934f5f2
Author: Yang Luo <[email protected]>
AuthorDate: Sat Apr 25 15:51:59 2026 +0800

    chore: fix ipMatch code
---
 src/util/ip.ts    | 24 ++++++++++-----
 test/util.test.ts | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 106 insertions(+), 7 deletions(-)

diff --git a/src/util/ip.ts b/src/util/ip.ts
index 855055d..de2310f 100644
--- a/src/util/ip.ts
+++ b/src/util/ip.ts
@@ -28,7 +28,7 @@
 
 import { Buffer } from 'buffer/';
 
-const ipv4Regex = /^(\d{1,3}\.){3,3}\d{1,3}$/;
+const ipv4Regex = 
/^(25[0-5]|2[0-4]\d|1\d\d|\d?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|\d?\d)){3}$/;
 const ipv6Regex = 
/^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i;
 
 export const ip = {
@@ -131,14 +131,13 @@ export const ip = {
   },
 
   isV6Format: function (ip: string): boolean {
-    return ipv6Regex.test(ip);
+    return ip.includes(':') && ipv6Regex.test(ip);
   },
 
   fromPrefixLen: function (prefixlen: number, family?: string): string {
+    family = _normalizeFamily(typeof family === 'string' ? family : 'ipv4');
     if (prefixlen > 32) {
       family = 'ipv6';
-    } else {
-      family = _normalizeFamily(typeof family === 'string' ? family : '');
     }
 
     let len = 4;
@@ -198,7 +197,7 @@ export const ip = {
   },
 
   subnet: function (addr: string, mask: string): any {
-    const networkAddress = ip.toLong(ip.mask(addr, mask));
+    const networkBuffer = ip.toBuffer(ip.mask(addr, mask));
 
     // Calculate the mask's length.
     const maskBuffer = ip.toBuffer(mask);
@@ -218,7 +217,17 @@ export const ip = {
 
     return {
       contains: function (other: string) {
-        return networkAddress === ip.toLong(ip.mask(other, mask));
+        let otherBuffer: Buffer;
+        try {
+          otherBuffer = ip.toBuffer(ip.mask(other, mask));
+        } catch {
+          return false;
+        }
+        if (networkBuffer.length !== otherBuffer.length) return false;
+        for (let i = 0; i < networkBuffer.length; i++) {
+          if (networkBuffer[i] !== otherBuffer[i]) return false;
+        }
+        return true;
       },
     };
   },
@@ -228,7 +237,8 @@ export const ip = {
     const addr = cidrParts[0];
     if (cidrParts.length !== 2) throw new Error('invalid CIDR subnet: ' + 
addr);
 
-    const mask = ip.fromPrefixLen(parseInt(cidrParts[1], 10));
+    const family = ip.isV6Format(addr) ? 'ipv6' : 'ipv4';
+    const mask = ip.fromPrefixLen(parseInt(cidrParts[1], 10), family);
 
     return ip.subnet(addr, mask);
   },
diff --git a/test/util.test.ts b/test/util.test.ts
index 8a5c3f8..c9b5ef8 100644
--- a/test/util.test.ts
+++ b/test/util.test.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import { getLogger, logPrint, Util as util } from '../src';
+import { ip } from '../src/util/ip';
 import { compile } from '@casbin/expression-eval';
 
 test('test enableLog success', () => {
@@ -145,6 +146,94 @@ test('test ipMatchFunc', () => {
   expect(util.ipMatchFunc('192.168.2.189', '192.168.1.134/26')).toEqual(false);
 });
 
+// CVE fix: IPv4 octet overflow allow-list bypass
+// Before fix: 448.168.2.10 was silently reduced to 192.168.2.10 (448 & 0xff = 
192),
+// allowing bypass of 192.168.2.0/24 allow-lists.
+test('test ipMatchFunc - IPv4 octet overflow is rejected', () => {
+  // Overflowed octets must be treated as invalid IPs, not silently normalized.
+  expect(() => util.ipMatchFunc('448.168.2.10', 
'192.168.2.0/24')).toThrow(Error);
+  expect(() => util.ipMatchFunc('266.0.0.1', '10.0.0.0/16')).toThrow(Error);
+  expect(() => util.ipMatchFunc('203.0.369.42', 
'203.0.113.0/24')).toThrow(Error);
+  // Exact /32 host rule bypass: 459.0.113.42 must NOT match 203.0.113.42/32
+  expect(() => util.ipMatchFunc('459.0.113.42', 
'203.0.113.42/32')).toThrow(Error);
+  // Boundary values: 255 is valid, 256 is not
+  expect(util.ipMatchFunc('10.0.255.1', '10.0.0.0/16')).toEqual(true);
+  expect(() => util.ipMatchFunc('10.0.256.1', '10.0.0.0/16')).toThrow(Error);
+});
+
+// CVE fix: IPv6 CIDR matching was completely broken — any string (including 
"abc")
+// matched any IPv6 CIDR policy due to toLong() collapsing everything to 0.
+test('test ipMatchFunc - IPv6 CIDR containment is correct', () => {
+  // Positive: addresses that should actually match
+  expect(util.ipMatchFunc('::1', '::1/128')).toEqual(true);
+  expect(util.ipMatchFunc('fe80::1', 'fe80::/10')).toEqual(true);
+  expect(util.ipMatchFunc('fe80::ffff', 'fe80::/10')).toEqual(true);
+  expect(util.ipMatchFunc('2001:db8::1', '2001:db8::/32')).toEqual(true);
+  expect(util.ipMatchFunc('fd00::1', 'fd00::/8')).toEqual(true);
+
+  // Negative: addresses outside the CIDR must not match
+  expect(util.ipMatchFunc('fe80::1', '::1/128')).toEqual(false);
+  expect(util.ipMatchFunc('ffff::1', '::1/128')).toEqual(false);
+  expect(util.ipMatchFunc('::', '::1/128')).toEqual(false);
+  expect(util.ipMatchFunc('2001:db8::1', 'fe80::/10')).toEqual(false);
+  expect(util.ipMatchFunc('::1', 'fe80::/10')).toEqual(false);
+  expect(util.ipMatchFunc('fe80::1', '2001:db8::/32')).toEqual(false);
+  expect(util.ipMatchFunc('::1', '2001:db8::/32')).toEqual(false);
+  expect(util.ipMatchFunc('::1', 'fd00::/8')).toEqual(false);
+  expect(util.ipMatchFunc('fe80::1', 'fd00::/8')).toEqual(false);
+});
+
+// CVE fix: garbage input like "abc" must be rejected, not silently match IPv6 
policies.
+test('test ipMatchFunc - non-IP strings are rejected as ip1', () => {
+  expect(() => util.ipMatchFunc('abc', '::1/128')).toThrow(Error);
+  expect(() => util.ipMatchFunc('abc', 'fe80::/10')).toThrow(Error);
+  expect(() => util.ipMatchFunc('abc', '2001:db8::/32')).toThrow(Error);
+  expect(() => util.ipMatchFunc('abc', 'fd00::/8')).toThrow(Error);
+  expect(() => util.ipMatchFunc('', '::1/128')).toThrow(Error);
+});
+
+test('test ip.isV4Format - octet range validation', () => {
+  // Valid addresses
+  expect(ip.isV4Format('0.0.0.0')).toEqual(true);
+  expect(ip.isV4Format('255.255.255.255')).toEqual(true);
+  expect(ip.isV4Format('192.168.1.1')).toEqual(true);
+  expect(ip.isV4Format('10.0.0.1')).toEqual(true);
+
+  // Overflowed octets must be rejected
+  expect(ip.isV4Format('256.0.0.1')).toEqual(false);
+  expect(ip.isV4Format('448.168.2.10')).toEqual(false);
+  expect(ip.isV4Format('266.0.0.1')).toEqual(false);
+  expect(ip.isV4Format('0.0.0.256')).toEqual(false);
+  expect(ip.isV4Format('999.999.999.999')).toEqual(false);
+});
+
+test('test ip.isV6Format - rejects non-IPv6 strings', () => {
+  // Valid IPv6 addresses
+  expect(ip.isV6Format('::1')).toEqual(true);
+  expect(ip.isV6Format('fe80::1')).toEqual(true);
+  expect(ip.isV6Format('2001:db8::')).toEqual(true);
+  expect(ip.isV6Format('::')).toEqual(true);
+
+  // Non-IPv6 strings must be rejected (no colon present)
+  expect(ip.isV6Format('abc')).toEqual(false);
+  expect(ip.isV6Format('a')).toEqual(false);
+  expect(ip.isV6Format('')).toEqual(false);
+  expect(ip.isV6Format('192.168.1.1')).toEqual(false);
+});
+
+test('test ip.cidrSubnet - IPv6 short prefixes use 16-byte mask', () => {
+  // /8, /10, /32 on IPv6 previously returned a 4-byte IPv4 mask
+  expect(ip.cidrSubnet('fd00::/8').contains('fd00::1')).toEqual(true);
+  expect(ip.cidrSubnet('fd00::/8').contains('fe80::1')).toEqual(false);
+  expect(ip.cidrSubnet('fe80::/10').contains('fe80::1')).toEqual(true);
+  expect(ip.cidrSubnet('fe80::/10').contains('::1')).toEqual(false);
+  expect(ip.cidrSubnet('2001:db8::/32').contains('2001:db8::1')).toEqual(true);
+  
expect(ip.cidrSubnet('2001:db8::/32').contains('2001:db9::1')).toEqual(false);
+  // Invalid input must not match
+  expect(ip.cidrSubnet('::1/128').contains('abc')).toEqual(false);
+  expect(ip.cidrSubnet('::1/128').contains('')).toEqual(false);
+});
+
 test('test globMatch', () => {
   expect(util.globMatch('/foo', '/foo')).toEqual(true);
   expect(util.globMatch('/foo', '/foo*')).toEqual(true);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to