Improved IPv6 address normalization in request-external#26749
Improved IPv6 address normalization in request-external#26749kevinansfield merged 1 commit intomainfrom
Conversation
After normalizing expanded IPv6 forms, re-check for IPv4-mapped addresses to ensure consistent private IP detection across all notation variants.
WalkthroughThe changes enhance the 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
ghost/core/core/server/lib/request-external.js (1)
142-158: Extract the IPv4-mapped parsing into one helper.This block duplicates the parsing/conversion logic already used earlier in
isPrivateIp. Keeping two copies of the same security check makes the pre- and post-normalization paths easy to drift apart.♻️ Refactor sketch
+function normalizeMappedIPv6ToIPv4(addr) { + const dottedMatch = addr.match(/^::ffff:(\d[\d.]+)$/i); + if (dottedMatch) { + return normalizeIPv4(dottedMatch[1]); + } + + const hexMatch = addr.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i); + if (hexMatch) { + const hi = parseInt(hexMatch[1], 16); + const lo = parseInt(hexMatch[2], 16); + return `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`; + } + + return undefined; +} + function isPrivateIp(addr) { - const v4DottedMatch = addr.match(/^::ffff:(\d[\d.]+)$/i); - if (v4DottedMatch) { - const normalized = normalizeIPv4(v4DottedMatch[1]); - if (normalized) { - return isPrivateIPv4(normalized); - } - return true; - } - - const v4HexMatch = addr.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i); - if (v4HexMatch) { - const hi = parseInt(v4HexMatch[1], 16); - const lo = parseInt(v4HexMatch[2], 16); - const mapped = ((hi >> 8) & 0xff) + '.' + (hi & 0xff) + '.' + ((lo >> 8) & 0xff) + '.' + (lo & 0xff); - return isPrivateIPv4(mapped); + const mappedIPv4 = normalizeMappedIPv6ToIPv4(addr); + if (mappedIPv4 !== undefined) { + return mappedIPv4 ? isPrivateIPv4(mappedIPv4) : true; } ... - const v4DottedNorm = normalized6.match(/^::ffff:(\d[\d.]+)$/i); - if (v4DottedNorm) { - const normV4 = normalizeIPv4(v4DottedNorm[1]); - if (normV4) { - return isPrivateIPv4(normV4); - } - return true; - } - const v4HexNorm = normalized6.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i); - if (v4HexNorm) { - const hi = parseInt(v4HexNorm[1], 16); - const lo = parseInt(v4HexNorm[2], 16); - const mapped = ((hi >> 8) & 0xff) + '.' + (hi & 0xff) + '.' + ((lo >> 8) & 0xff) + '.' + (lo & 0xff); - return isPrivateIPv4(mapped); + const normalizedMappedIPv4 = normalizeMappedIPv6ToIPv4(normalized6); + if (normalizedMappedIPv4 !== undefined) { + return normalizedMappedIPv4 ? isPrivateIPv4(normalizedMappedIPv4) : true; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ghost/core/core/server/lib/request-external.js` around lines 142 - 158, The duplicated IPv4-mapped IPv6 parsing in request-external.js should be extracted into a single helper (e.g., parseMappedIPv4FromNormalized6 or extractMappedIPv4) that encapsulates both dotted-decimal and hex-pair decoding using normalizeIPv4 logic, returning either a normalized IPv4 string or null; replace the duplicated blocks in isPrivateIp (pre-normalization) and the post-normalization checks (the v4DottedNorm and v4HexNorm logic) to call this helper and then call isPrivateIPv4 on its result, removing the repeated parsing/conversion code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ghost/core/test/unit/server/lib/request-external.test.js`:
- Around line 169-180: The test only covers dotted IPv4-mapped IPv6 forms, but
the code also handles hex-pair mapped forms (post-normalization
::ffff:<hi>:<lo>), so add assertions to request-external.test.js around the
isPrivateIp tests to cover expanded hex-mapped cases: include private examples
such as '0:0:0:0:0:ffff:7f00:0001' and '0000:0000:0000:0000:0000:ffff:ac10:0001'
(expected true) and public examples such as '0:0:0:0:0:ffff:0808:0808' and
'0000:0000:0000:0000:0000:ffff:0808:0808' (expected false) so the isPrivateIp
code path that handles ::ffff:<hi>:<lo> is exercised.
---
Nitpick comments:
In `@ghost/core/core/server/lib/request-external.js`:
- Around line 142-158: The duplicated IPv4-mapped IPv6 parsing in
request-external.js should be extracted into a single helper (e.g.,
parseMappedIPv4FromNormalized6 or extractMappedIPv4) that encapsulates both
dotted-decimal and hex-pair decoding using normalizeIPv4 logic, returning either
a normalized IPv4 string or null; replace the duplicated blocks in isPrivateIp
(pre-normalization) and the post-normalization checks (the v4DottedNorm and
v4HexNorm logic) to call this helper and then call isPrivateIPv4 on its result,
removing the repeated parsing/conversion code paths.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: eaebacf3-4807-4032-b67b-cc559d92832c
📒 Files selected for processing (2)
ghost/core/core/server/lib/request-external.jsghost/core/test/unit/server/lib/request-external.test.js
| it('detects expanded IPv4-mapped IPv6 addresses as private (dotted notation)', function () { | ||
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:127.0.0.1'), true); | ||
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:10.0.0.1'), true); | ||
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:192.168.0.1'), true); | ||
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:169.254.169.254'), true); | ||
| assert.equal(isPrivateIp('0000:0000:0000:0000:0000:ffff:127.0.0.1'), true); | ||
| }); | ||
|
|
||
| it('allows public expanded IPv4-mapped IPv6 addresses', function () { | ||
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:8.8.8.8'), false); | ||
| assert.equal(isPrivateIp('0000:0000:0000:0000:0000:ffff:8.8.8.8'), false); | ||
| }); |
There was a problem hiding this comment.
Add expanded hex-mapped IPv6 cases as well.
These assertions only cover 0:0:0:0:0:ffff:<ipv4> inputs. The implementation also added a separate post-normalization ::ffff:<hi>:<lo> path, so a regression there would still pass this suite.
🧪 Suggested coverage
+ it('detects expanded IPv4-mapped IPv6 addresses as private (hex notation)', function () {
+ assert.equal(isPrivateIp('0:0:0:0:0:ffff:7f00:1'), true); // 127.0.0.1
+ assert.equal(isPrivateIp('0:0:0:0:0:ffff:c0a8:1'), true); // 192.168.0.1
+ });
+
+ it('allows public expanded IPv4-mapped IPv6 addresses (hex notation)', function () {
+ assert.equal(isPrivateIp('0:0:0:0:0:ffff:808:808'), false); // 8.8.8.8
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('detects expanded IPv4-mapped IPv6 addresses as private (dotted notation)', function () { | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:127.0.0.1'), true); | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:10.0.0.1'), true); | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:192.168.0.1'), true); | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:169.254.169.254'), true); | |
| assert.equal(isPrivateIp('0000:0000:0000:0000:0000:ffff:127.0.0.1'), true); | |
| }); | |
| it('allows public expanded IPv4-mapped IPv6 addresses', function () { | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:8.8.8.8'), false); | |
| assert.equal(isPrivateIp('0000:0000:0000:0000:0000:ffff:8.8.8.8'), false); | |
| }); | |
| it('detects expanded IPv4-mapped IPv6 addresses as private (dotted notation)', function () { | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:127.0.0.1'), true); | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:10.0.0.1'), true); | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:192.168.0.1'), true); | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:169.254.169.254'), true); | |
| assert.equal(isPrivateIp('0000:0000:0000:0000:0000:ffff:127.0.0.1'), true); | |
| }); | |
| it('allows public expanded IPv4-mapped IPv6 addresses', function () { | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:8.8.8.8'), false); | |
| assert.equal(isPrivateIp('0000:0000:0000:0000:0000:ffff:8.8.8.8'), false); | |
| }); | |
| it('detects expanded IPv4-mapped IPv6 addresses as private (hex notation)', function () { | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:7f00:1'), true); // 127.0.0.1 | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:c0a8:1'), true); // 192.168.0.1 | |
| }); | |
| it('allows public expanded IPv4-mapped IPv6 addresses (hex notation)', function () { | |
| assert.equal(isPrivateIp('0:0:0:0:0:ffff:808:808'), false); // 8.8.8.8 | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ghost/core/test/unit/server/lib/request-external.test.js` around lines 169 -
180, The test only covers dotted IPv4-mapped IPv6 forms, but the code also
handles hex-pair mapped forms (post-normalization ::ffff:<hi>:<lo>), so add
assertions to request-external.test.js around the isPrivateIp tests to cover
expanded hex-mapped cases: include private examples such as
'0:0:0:0:0:ffff:7f00:0001' and '0000:0000:0000:0000:0000:ffff:ac10:0001'
(expected true) and public examples such as '0:0:0:0:0:ffff:0808:0808' and
'0000:0000:0000:0000:0000:ffff:0808:0808' (expected false) so the isPrivateIp
code path that handles ::ffff:<hi>:<lo> is exercised.
ref https://linear.app/ghost/issue/ONC-1533/ After normalizing expanded IPv6 forms, re-check for IPv4-mapped addresses to ensure consistent private IP detection across all notation variants.
After normalizing expanded IPv6 forms, re-check for IPv4-mapped
addresses to ensure consistent private IP detection across all
notation variants.