Skip to content

Improved IPv6 address normalization in request-external#26749

Merged
kevinansfield merged 1 commit intomainfrom
ipv6-address-normalization
Mar 10, 2026
Merged

Improved IPv6 address normalization in request-external#26749
kevinansfield merged 1 commit intomainfrom
ipv6-address-normalization

Conversation

@kevinansfield
Copy link
Copy Markdown
Member

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.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 10, 2026

Walkthrough

The changes enhance the isPrivateIp function to improve handling of IPv4-mapped IPv6 addresses. The implementation now detects and normalizes IPv4-mapped IPv6 addresses in both dotted notation (::ffff:) and hexadecimal form (::ffff:<hi 16-bit>:<lo 16-bit>), converts them to IPv4, and evaluates their private status using the existing IPv4 private range check. The logic is integrated into the existing IPv6 validation flow. Corresponding test cases were added to verify behavior with expanded IPv4-mapped IPv6 addresses in both private and public address ranges.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main change: improving IPv6 address normalization in the request-external module by re-checking for IPv4-mapped addresses.
Description check ✅ Passed The description clearly relates to the changeset, explaining the purpose of re-checking for IPv4-mapped addresses after normalization to ensure consistent private IP detection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ipv6-address-normalization

Comment @coderabbitai help to get the list of available commands and usage tips.

@kevinansfield kevinansfield enabled auto-merge (squash) March 10, 2026 11:46
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 292bdc3 and 26ba0ed.

📒 Files selected for processing (2)
  • ghost/core/core/server/lib/request-external.js
  • ghost/core/test/unit/server/lib/request-external.test.js

Comment on lines +169 to +180
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);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@kevinansfield kevinansfield merged commit 9b7f221 into main Mar 10, 2026
31 checks passed
@kevinansfield kevinansfield deleted the ipv6-address-normalization branch March 10, 2026 12:12
peterzimon pushed a commit that referenced this pull request Mar 10, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant