Skip to content

fix(security): enforce SSRF ipBlackList on IPv6 addresses#5967

Draft
fengmk2 wants to merge 1 commit into
nextfrom
fix/security-ssrf-ipv6
Draft

fix(security): enforce SSRF ipBlackList on IPv6 addresses#5967
fengmk2 wants to merge 1 commit into
nextfrom
fix/security-ssrf-ipv6

Conversation

@fengmk2

@fengmk2 fengmk2 commented Jun 10, 2026

Copy link
Copy Markdown
Member

Problem

The SSRF address check (config.security.ssrf -> generated checkAddress) skipped every IPv6 address and then fell through to "allow":

if (ipAddress.family === 6) {
  continue;
}

So a hostname that resolves to an internal IPv6 address (or an IPv4-mapped IPv6 such as ::ffff:127.0.0.1, reachable via an attacker-controlled DNS record) bypassed ssrf.ipBlackList entirely. On top of that, @eggjs/ip's cidrSubnet().contains() is IPv4-only (it relies on 32-bit toLong), so it cannot evaluate IPv6 CIDR ranges even if the address were checked.

Fix

  • Stop skipping IPv6 addresses; match them against the configured exception/black lists.
  • Replace getContains with a buffer-based prefix comparison that is correct for both IPv4 and IPv6 (and rejects cross-family comparisons).
  • Also check IPv4-mapped IPv6 addresses against IPv4 rules, so ::ffff:127.0.0.1 is caught by a 127.0.0.1 entry.

Test

Added unit tests on the generated checkAddress covering: IPv6 addresses in the blacklist, IPv4-mapped IPv6 against IPv4 rules, and ipExceptionList for IPv6. All fail before the fix (addresses were allowed) and pass after. Existing IPv4 behavior is unchanged.

plugins/security suite: 201 passed, 4 skipped.

The SSRF address check skipped every IPv6 address and then fell through
to "allow", so a hostname resolving to an internal IPv6 address (or an
IPv4-mapped IPv6 such as `::ffff:127.0.0.1`) bypassed `ssrf.ipBlackList`.
`@eggjs/ip`'s `cidrSubnet().contains()` is also IPv4-only, so it cannot
evaluate IPv6 ranges at all.

Match IPv6 addresses against the configured rules with a buffer-based
prefix comparison, and also check IPv4-mapped IPv6 addresses against
IPv4 rules.
@fengmk2 fengmk2 self-assigned this Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eda39ad8-7e4e-44e1-8fab-94a877075211

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/security-ssrf-ipv6

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces IPv6 support for SSRF protection in the security plugin. It adds handling for IPv4-mapped IPv6 addresses to prevent SSRF bypasses and refactors IP and CIDR subnet checks to use normalized buffer comparisons. The review feedback highlights critical security and robustness improvements: replacing the regex-based IPv4-mapped IPv6 detection with a robust buffer-based check to prevent bypasses via alternative textual representations, validating CIDR prefix lengths to avoid incorrect matches, removing the now-redundant regex, and expanding test coverage to include alternative IPv6 address formats.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +155 to 159
const candidates = [address];
const mapped = IPV4_MAPPED_RE.exec(address);
if (mapped) {
candidates.push(mapped[1]);
}

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.

security-high high

Using a regular expression to detect and extract IPv4-mapped IPv6 addresses is prone to bypasses because IPv6 addresses can be represented in multiple valid textual formats (e.g., 0:0:0:0:0:ffff:127.0.0.1 or ::ffff:7f00:1). An attacker could potentially use an alternative representation to bypass the regex check while still targeting the internal IPv4 address.

Instead, we can perform a robust buffer-based check. Since IP.toBuffer normalizes all valid representations of an IP address, we can check if the resulting 16-byte buffer represents an IPv4-mapped IPv6 address (where the first 10 bytes are 0 and the next 2 bytes are 0xff), and then extract the IPv4 address directly from the last 4 bytes.

        const candidates = [address];
        const addressBuffer = toBufferOrNull(address);
        if (addressBuffer && addressBuffer.length === 16) {
          let isMapped = addressBuffer[10] === 0xff && addressBuffer[11] === 0xff;
          if (isMapped) {
            for (let i = 0; i < 10; i++) {
              if (addressBuffer[i] !== 0) {
                isMapped = false;
                break;
              }
            }
          }
          if (isMapped) {
            candidates.push(`${addressBuffer[12]}.${addressBuffer[13]}.${addressBuffer[14]}.${addressBuffer[15]}`);
          }
        }

Comment on lines +228 to +233
const [base, prefix] = rule.split('/');
const prefixLength = Number.parseInt(prefix, 10);
if (!base || Number.isNaN(prefixLength)) {
throw new Error(`invalid CIDR subnet: ${rule}`);
}
const baseBuffer = IP.toBuffer(base);

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.

high

The parsed prefixLength is not validated to ensure it is within valid bounds. If a user configures an invalid CIDR with a negative prefix length (e.g., 1.2.3.4/-5), prefixLength will be negative, causing isInSubnet to skip the comparison loop and incorrectly return true (matching any IP address). Similarly, a prefix length greater than the address's bit length (32 for IPv4, 128 for IPv6) is invalid.

We should validate that prefixLength is non-negative and does not exceed the maximum bit length of the IP family.

  const [base, prefix] = rule.split('/');
  const prefixLength = Number.parseInt(prefix, 10);
  if (!base || Number.isNaN(prefixLength)) {
    throw new Error(`invalid CIDR subnet: ${rule}`);
  }
  const baseBuffer = IP.toBuffer(base);
  if (prefixLength < 0 || prefixLength > baseBuffer.length * 8) {
    throw new Error(`invalid CIDR subnet: ${rule}`);
  }

Comment on lines +62 to +63
// IPv4-mapped IPv6 address, e.g. `::ffff:127.0.0.1`
const IPV4_MAPPED_RE = /^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i;

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.

medium

Since we are switching to a robust buffer-based check to detect IPv4-mapped IPv6 addresses, the IPV4_MAPPED_RE regular expression is no longer needed and can be safely removed.

Comment on lines +242 to +246
it('should block IPv4-mapped IPv6 address against IPv4 blacklist rules', () => {
const checkAddress = buildCheckAddress(['127.0.0.1', '10.0.0.0/8']);
expect(checkAddress([{ address: '::ffff:127.0.0.1', family: 6 }], 6, 'evil.example.com')).toBe(false);
expect(checkAddress([{ address: '::ffff:10.1.2.3', family: 6 }], 6, 'evil.example.com')).toBe(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.

medium

To ensure the robustness of the buffer-based IPv4-mapped IPv6 detection, we should add test cases covering alternative valid representations of IPv4-mapped IPv6 addresses (such as using full zero expansion or hex representation for the IPv4 part).

  it('should block IPv4-mapped IPv6 address against IPv4 blacklist rules', () => {
    const checkAddress = buildCheckAddress(['127.0.0.1', '10.0.0.0/8']);
    expect(checkAddress([{ address: '::ffff:127.0.0.1', family: 6 }], 6, 'evil.example.com')).toBe(false);
    expect(checkAddress([{ address: '::ffff:10.1.2.3', family: 6 }], 6, 'evil.example.com')).toBe(false);
    expect(checkAddress([{ address: '0:0:0:0:0:ffff:127.0.0.1', family: 6 }], 6, 'evil.example.com')).toBe(false);
    expect(checkAddress([{ address: '::ffff:7f00:1', family: 6 }], 6, 'evil.example.com')).toBe(false);
  });

@codecov

codecov Bot commented Jun 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.34%. Comparing base (1836bce) to head (904676d).

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5967      +/-   ##
==========================================
+ Coverage   85.32%   85.34%   +0.02%     
==========================================
  Files         670      670              
  Lines       19553    19582      +29     
  Branches     3864     3870       +6     
==========================================
+ Hits        16683    16712      +29     
- Misses       2479     2480       +1     
+ Partials      391      390       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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