Skip to content

[ANCHOR-1290]: SEP-10 client_domain SSRF: ANCHOR-1236 blocklist omits NAT64 (64:ff9b::/96) and 6to4 (2002::/16), re-reaching internal IPv4 on mainnet - #2007

Open
amandagonsalves wants to merge 6 commits into
developfrom
fix/anchor-1290
Open

[ANCHOR-1290]: SEP-10 client_domain SSRF: ANCHOR-1236 blocklist omits NAT64 (64:ff9b::/96) and 6to4 (2002::/16), re-reaching internal IPv4 on mainnet#2007
amandagonsalves wants to merge 6 commits into
developfrom
fix/anchor-1290

Conversation

@amandagonsalves

Copy link
Copy Markdown
Collaborator

Description

ANCHOR-1236 added ClientDomainHelper.isNonPublicAddress as a private-network guard so the unauthenticated GET /auth (and SEP-45 equivalent) client_domain fetch can't be pointed at an internal host. That guard blocks IPv4 private ranges, loopback, link-local, fc00::/7, and IPv4-mapped IPv6 (::ffff:a.b.c.d, auto-normalized to Inet4Address by the JDK), but never unwraps the two IPv6 transition ranges that carry a real IPv4 payload in their low bits: NAT64 (64:ff9b::/96, RFC 6052) and 6to4 (2002::/16, RFC 3056). Neither range matches any of the eight existing predicates, so a client_domain resolving to 64:ff9b::<internal-ipv4> or 2002:<internal-ipv4>:: passes both call sites that delegate to this classifier — the pre-fetch check and the connection-time DNS validator — and the platform connects through the operator's own NAT64/6to4 gateway to the embedded internal address. 0.0.0.0/8 had the same gap: only the exact 0.0.0.0 address was caught.

Fixing this by only unwrapping and re-checking the embedded IPv4 isn't quite enough on its own: RFC 6052 leaves a NAT64 gateway's handling of a private embedded address implementation-defined, and a gateway could also forward to a public IP the platform's network trusts only via that internal path. So both prefixes are also blocked outright, in addition to the unwrap-and-recheck.

Changes

  • ClientDomainHelper.isNonPublicAddress: unwraps NAT64, 6to4, and the deprecated IPv4-compatible IPv6 form (::a.b.c.d) via a new unwrapEmbeddedIPv4 before running the existing IPv4 predicates against the real embedded address; adds isThisNetwork for 0.0.0.0/8; blocks the NAT64/6to4 prefixes outright via new isNat64WellKnown/is6to4 predicates, independent of what the embedded address resolves to.
  • SsrfBlocklistBypassTest (new): runs the reporter's own reproduction directly against ClientDomainHelper.validateDomainNotPrivateNetwork — a control case confirming existing ranges stay blocked, and cases confirming the NAT64/6to4/0.0.0.0/8 addresses are now blocked.

Acceptance Criteria

  • client_domain resolving to 64:ff9b::<internal-ipv4> (NAT64) is rejected by both validateDomainNotPrivateNetwork and validatingDns.
  • client_domain resolving to 2002:<internal-ipv4>:: (6to4) is rejected the same way.
  • client_domain resolving to any 0.0.0.0/8 address is rejected.
  • Existing blocked ranges (RFC1918, loopback, link-local, fc00::/7, CGNAT, IPv4-mapped) remain blocked.
  • SEP-45's auth endpoint, which shares this same guard, gets the fix with no separate change.

Context

HackerOne #3968541

Testing

  • Unit: ./gradlew :core:test --tests "org.stellar.anchor.util.SsrfBlocklistBypassTest"
  • Unit: ./gradlew :core:test --tests "org.stellar.anchor.util.ClientDomainHelperTest"
  • Integration: ./gradlew :core:test (full module regression, no failures)

Documentation

N/A

Known limitations

Doesn't change Sep10Service.validateChallengeRequestClient's default of accepting any client_domain when sep10.client_attribution_required is unset and sep10.client_allow_list is empty. The report frames an allow-list as a recommended defense-in-depth improvement, not a required part of this fix — it's a separate, broader policy change affecting every deployment's default config, not just attacker-controlled input, and deserves its own scoping.

* ClientDomainHelper.isNonPublicAddress now unwraps NAT64 (64:ff9b::/96),
  6to4 (2002::/16), and the deprecated IPv4-compatible IPv6 form before
  running the existing IPv4 checks against the real embedded address
* blocks the NAT64/6to4 prefixes outright as defense in depth, even when
  the embedded address looks public
* adds 0.0.0.0/8 coverage
* adds SsrfBlocklistBypassTest covering the previously-missed ranges
Copilot AI balanced review requested due to automatic review settings September 2, 2026 21:41

Copilot AI 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.

Pull request overview

Strengthens client_domain SSRF protection for embedded IPv4 transition addresses.

Changes:

  • Blocks NAT64, 6to4, and 0.0.0.0/8.
  • Adds embedded IPv4 unwrapping and regression tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
ClientDomainHelper.java Extends the non-public address classifier.
SsrfBlocklistBypassTest.kt Adds SSRF blocklist regression coverage.
Suppressed comments (2)

core/src/test/kotlin/org/stellar/anchor/util/SsrfBlocklistBypassTest.kt:38

  • Every NAT64 case embeds a non-public IPv4, so the test still passes if the explicit prefix block is removed and only unwrap-and-recheck remains. Include a public payload to cover the stated requirement that 64:ff9b::/96 is blocked outright.
    for (h in listOf("64:ff9b::a9fe:a9fe", "64:ff9b::a00:1", "64:ff9b::7f00:1")) {

core/src/test/kotlin/org/stellar/anchor/util/SsrfBlocklistBypassTest.kt:48

  • Both 6to4 cases embed non-public IPv4 addresses, so they do not distinguish the explicit 2002::/16 block from unwrap-and-recheck alone. Include a public embedded address to lock in the intended outright prefix rejection.
    for (h in listOf("2002:a00:1::", "2002:7f00:1::")) {

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread core/src/main/java/org/stellar/anchor/util/ClientDomainHelper.java Outdated
* isIpv4Compatible now requires bytes 10-11 to be zero, matching only
  the true ::/96 range instead of misclassifying most of ::/80 by its
  final 32 bits
* add a real ::/96 literal and a regression case proving an ordinary
  IPv6 address is no longer misclassified
Copilot AI review requested due to automatic review settings September 3, 2026 21:25
@amandagonsalves amandagonsalves self-assigned this Sep 3, 2026

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread core/src/main/java/org/stellar/anchor/util/ClientDomainHelper.java
* isNonPublicAddress now also blocks 64:ff9b:1::/48 outright, closing
  the same embedded-IPv4 SSRF path on deployments using the local-use
  NAT64 prefix instead of the well-known 64:ff9b::/96 one
* add a regression case for the /48 prefix
Copilot AI review requested due to automatic review settings September 4, 2026 16:14

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

core/src/test/kotlin/org/stellar/anchor/util/SsrfBlocklistBypassTest.kt:40

  • The current cases all embed non-public IPv4 addresses, so they still pass if the unconditional NAT64-prefix check is accidentally removed and only unwrapEmbeddedIPv4 remains. Add a public payload such as 8.8.8.8 to verify the stated requirement that 64:ff9b::/96 is blocked outright.
    for (h in listOf("64:ff9b::a9fe:a9fe", "64:ff9b::a00:1", "64:ff9b::7f00:1")) {

core/src/test/kotlin/org/stellar/anchor/util/SsrfBlocklistBypassTest.kt:50

  • Both cases embed non-public IPv4 addresses and therefore only prove that unwrapping works; they do not exercise the independent 2002::/16 block. Include a public embedded address so a regression that removes the outright 6to4-prefix check is detected.
    for (h in listOf("2002:a00:1::", "2002:7f00:1::")) {

Copilot AI review requested due to automatic review settings September 8, 2026 17:51

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings September 14, 2026 13:43

Copilot AI 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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (2)

core/src/test/kotlin/org/stellar/anchor/util/SsrfBlocklistBypassTest.kt:40

  • These cases all embed addresses that the existing IPv4 predicates reject, so they still pass if the new outright NAT64-prefix check is removed. Add a case embedding a public IPv4 address (for example 64:ff9b::808:808) to cover the stated requirement that the prefix is blocked independently of its payload.
    for (h in listOf("64:ff9b::a9fe:a9fe", "64:ff9b::a00:1", "64:ff9b::7f00:1")) {

core/src/test/kotlin/org/stellar/anchor/util/SsrfBlocklistBypassTest.kt:50

  • Both cases are rejected after unwrapping because their embedded IPv4 addresses are already non-public; they do not exercise the new requirement to reject all of 2002::/16. Include a public payload such as 2002:808:808:: so removal of the outright 6to4 check cannot leave this security behavior untested.
    for (h in listOf("2002:a00:1::", "2002:7f00:1::")) {

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.

3 participants