From 1ec87945ce865632feb6733fa699ae539c3091ad Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Thu, 17 Sep 2026 10:39:00 +0800 Subject: [PATCH 1/7] Fix(ipcalculator): parse octal input directly as BigInt Avoid Number overflow when parsing long standalone or dotted octal input. Cover oversized values, leading zeros, and IPv4 boundaries. Co-Authored-By: GPT-6 --- frontend/utils/ip-calc.js | 4 ++-- tests/ip-calc.test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/frontend/utils/ip-calc.js b/frontend/utils/ip-calc.js index 511b06f31..8df1feeda 100644 --- a/frontend/utils/ip-calc.js +++ b/frontend/utils/ip-calc.js @@ -476,7 +476,7 @@ const parseInetAton = (input) => { const notations = new Set(); const numbers = parts.map((p) => { if (/^0x/i.test(p)) { notations.add('hex'); return BigInt(p); } - if (p.length > 1 && p.startsWith('0')) { notations.add('octal'); return BigInt(parseInt(p, 8)); } + if (p.length > 1 && p.startsWith('0')) { notations.add('octal'); return BigInt(`0o${p}`); } return BigInt(p); }); const last = numbers.pop(); @@ -563,7 +563,7 @@ export const classifyInput = (raw) => { // A leading zero reads as octal, as inet_aton would. if (/^0[0-7]+$/.test(input)) { - const value = BigInt(parseInt(input, 8)); + const value = BigInt(`0o${input}`); if (value > MAX_V4) return invalid(raw, input, 'integer-too-large'); return { ...base, kind: 'ipv4', value, notation: 'octal', obfuscated: true }; } diff --git a/tests/ip-calc.test.js b/tests/ip-calc.test.js index 8e64c255a..2ab1764f9 100644 --- a/tests/ip-calc.test.js +++ b/tests/ip-calc.test.js @@ -379,6 +379,37 @@ describe('classifyInput', () => { assert.equal(classifyInput('123456789012').kind, 'integer'); }); + it('rejects oversized octal inputs without passing through Number', () => { + for (const length of [20, 400, 10000]) { + const octal = `0${'7'.repeat(length)}`; + for (const [input, reason] of [ + [octal, 'integer-too-large'], + [`${octal}.0.0.1`, 'unrecognized'], + [`127.${octal}`, 'unrecognized'], + [`0x7f.0.${octal}`, 'unrecognized'], + ]) { + for (const parse of [classifyInput, calculate]) { + const result = parse(input); + assert.equal(result.kind, 'invalid'); + assert.equal(result.reason, reason); + } + } + } + }); + + it('keeps octal boundaries and arbitrarily padded valid values exact', () => { + for (const [input, value] of [ + ['037777777777', 0xffffffffn], + ['0377.0377.0377.0377', 0xffffffffn], + [`${'0'.repeat(400)}177`, 127n], + [`${'0'.repeat(400)}177.0.0.1`, 0x7f000001n], + ]) { + assert.equal(calculate(input).value, value); + } + assert.equal(calculate('040000000000').kind, 'invalid'); + assert.equal(calculate('0400.0.0.1').kind, 'invalid'); + }); + it('normalises ranges', () => { const r = classifyInput('192.0.2.100-192.0.2.1'); assert.equal(r.reversed, true); From 6de54d7e4494bd1b19ca645fbdf18275a535483f Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Thu, 17 Sep 2026 10:39:40 +0800 Subject: [PATCH 2/7] Fix(ip): reject out-of-family prefix containment values Validate both network and address against the selected IP family. Cover negative values, overflow, and valid IPv4 and IPv6 boundaries. Co-Authored-By: GPT-6 --- common/ip-math.js | 3 ++- tests/ip-math.test.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/common/ip-math.js b/common/ip-math.js index da25272dd..801055189 100644 --- a/common/ip-math.js +++ b/common/ip-math.js @@ -247,7 +247,8 @@ export const cidrInfo = (str) => { /* ------------------------------------------------------------------ */ export const prefixContains = (network, prefix, family, value) => { - if (!isFamily(family) || !isPrefix(prefix, family) || !isBig(network) || !isBig(value)) return false; + if (!isFamily(family) || !isPrefix(prefix, family) + || !inFamily(network, family) || !inFamily(value, family)) return false; const shift = BigInt(BITS[family] - prefix); return (value >> shift) === (network >> shift); }; diff --git a/tests/ip-math.test.js b/tests/ip-math.test.js index 06cfda90e..acff368bb 100644 --- a/tests/ip-math.test.js +++ b/tests/ip-math.test.js @@ -260,6 +260,23 @@ describe('containment & ordering', () => { assert.equal(prefixContains(v4('10.0.0.0'), 8, 4, 5), false); assert.equal(prefixContains(v4('10.0.0.0'), 33, 4, v4('10.1.2.3')), false); }); + it('prefixContains rejects negative and overflowing addresses in either position', () => { + for (const [family, bits] of [[4, 32], [6, 128]]) { + const overflow = 1n << BigInt(bits); + const max = overflow - 1n; + assert.equal(prefixContains(0n, 0, family, max), true); + assert.equal(prefixContains(max, bits, family, max), true); + for (const prefix of [0, 1, bits]) { + for (const invalid of [-1n, overflow, overflow + 1n]) { + assert.equal(prefixContains(invalid, prefix, family, invalid), false); + assert.equal(prefixContains(invalid, prefix, family, 0n), false); + assert.equal(prefixContains(0n, prefix, family, invalid), false); + } + } + assert.equal(prefixContains(-1n, 0, family, -2n), false); + assert.equal(prefixContains(overflow, 0, family, overflow + 1n), false); + } + }); it('cidrOverlaps', () => { assert.equal(cidrOverlaps('10.0.0.0/8', '10.1.0.0/16'), true); assert.equal(cidrOverlaps('10.1.0.0/16', '10.0.0.0/8'), true); From 0c6e5f2c7a67c785db38313adcc3f9be4657088b Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Thu, 17 Sep 2026 10:39:58 +0800 Subject: [PATCH 3/7] Fix(ip): validate subnet split limits before conversion Return null for invalid options or limits instead of throwing during BigInt conversion. Preserve zero, default, and safe integer limits. Co-Authored-By: GPT-6 --- common/ip-math.js | 5 ++++- tests/ip-math.test.js | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/common/ip-math.js b/common/ip-math.js index 801055189..aedf2d0fc 100644 --- a/common/ip-math.js +++ b/common/ip-math.js @@ -281,7 +281,10 @@ export const compareIps = (a, b) => { // Children of `cidrStr` at `newPrefix`. `total` is exact; `subnets` stops at // `limit` so a /8 → /32 request never materialises 16M strings. -export const splitCidr = (cidrStr, newPrefix, { limit = 1024 } = {}) => { +export const splitCidr = (cidrStr, newPrefix, options = {}) => { + if (!options || typeof options !== 'object' || Array.isArray(options)) return null; + const { limit = 1024 } = options; + if (!Number.isSafeInteger(limit) || limit < 0) return null; const cidr = parseCidr(cidrStr); if (!cidr || !isPrefix(newPrefix, cidr.family) || newPrefix < cidr.prefix) return null; const { family, network } = cidr; diff --git a/tests/ip-math.test.js b/tests/ip-math.test.js index acff368bb..0e4bc4b5e 100644 --- a/tests/ip-math.test.js +++ b/tests/ip-math.test.js @@ -293,6 +293,29 @@ describe('containment & ordering', () => { }); describe('splitCidr', () => { + it('rejects invalid limits and options without coercing them', () => { + const limits = [Infinity, -Infinity, NaN, 1.5, -1, Number.MAX_SAFE_INTEGER + 1, + Symbol('limit'), '4', 4n, null, true, {}, []]; + for (const limit of limits) { + assert.equal(splitCidr('10.0.0.0/24', 26, { limit }), null); + assert.equal(splitCidr('2001:db8::/32', 48, { limit }), null); + } + for (const options of [null, false, 5, '4', 4n, Symbol('options'), []]) { + assert.equal(splitCidr('10.0.0.0/24', 26, options), null); + } + }); + it('accepts zero, default and large safe limits without changing the exact total', () => { + const zero = splitCidr('10.0.0.0/24', 26, { limit: 0 }); + assert.deepEqual(zero.subnets, []); + assert.equal(zero.total, 4n); + assert.equal(zero.truncated, true); + for (const limit of [undefined, 4, Number.MAX_SAFE_INTEGER]) { + const result = splitCidr('10.0.0.0/24', 26, { limit }); + assert.equal(result.subnets.length, 4); + assert.equal(result.total, 4n); + assert.equal(result.truncated, false); + } + }); it('splits a /24 into /26s', () => { const r = splitCidr('10.0.0.0/24', 26); assert.deepEqual(r.subnets, ['10.0.0.0/26', '10.0.0.64/26', '10.0.0.128/26', '10.0.0.192/26']); From 1adefb13d297f898c1140219eb57bca9f5b9252e Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Thu, 17 Sep 2026 10:57:30 +0800 Subject: [PATCH 4/7] Improvements --- frontend/components/ui/slider/Slider.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/ui/slider/Slider.vue b/frontend/components/ui/slider/Slider.vue index da00e4290..4b7ea4ed3 100644 --- a/frontend/components/ui/slider/Slider.vue +++ b/frontend/components/ui/slider/Slider.vue @@ -60,7 +60,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits); v-for="(_, key) in modelValue" :key="key" data-slot="slider-thumb" - class="bg-white border-primary ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" + class="bg-white border-primary ring-ring/50 block size-4 shrink-0 rounded-full border shadow-sm transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 cursor-col-resize" /> From 40342a9631bd0e8ca6443f22449a157f89d8de79 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Fri, 18 Sep 2026 10:00:47 +0800 Subject: [PATCH 5/7] Improvements --- frontend/components/widgets/InfoBanner.vue | 152 ++++++++++--------- frontend/utils/banners.js | 54 +++++-- tests/banners.test.js | 162 ++++++++++++++++----- 3 files changed, 249 insertions(+), 119 deletions(-) diff --git a/frontend/components/widgets/InfoBanner.vue b/frontend/components/widgets/InfoBanner.vue index 138f38024..b2569d96f 100644 --- a/frontend/components/widgets/InfoBanner.vue +++ b/frontend/components/widgets/InfoBanner.vue @@ -1,39 +1,44 @@ - +