Skip to content

Commit bbbb5f7

Browse files
wyattwalterclaude
andcommitted
fix: use the correct RFC 6052 layout for NAT64 /48 local-use addresses
The NAT64 match covered all of 64:ff9b::/32 but always read the embedded IPv4 from the low 32 bits. That is the layout for the well-known 64:ff9b::/96 prefix only. For the RFC 8215 local-use prefix 64:ff9b:1::/48, RFC 6052 places the IPv4 in bytes 6-7 and 9-10 (byte 8 is the reserved u-octet). Reading the low bits for a /48 address is wrong in both directions: a /48 address embedding an internal destination while carrying a routable value in its low 32 bits was read as routable and allowed through, and a /48 address embedding a routable destination whose low bits are zero was misread as 0.0.0.0 and blocked. Split the match into the well-known /96 (which now also requires bytes 4-11 to be zero) and the local-use /48, and reassemble the /48 IPv4 from its correct bytes. Other RFC 6052 prefix lengths use a Network-Specific Prefix that cannot be recognized from the address alone and remain out of scope, as noted. Tests confirmed red against the pre-fix source (2 bypass cases, 1 over-block). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ec4186d commit bbbb5f7

2 files changed

Lines changed: 58 additions & 9 deletions

File tree

app/server/appsmith-interfaces/src/main/java/com/appsmith/util/RestrictedHostFilter.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -803,16 +803,26 @@ private static InetAddress parseIpLiteral(String host) throws UnknownHostExcepti
803803
* none. Covers the embeddings where the IPv6 address is purely an encoding of an IPv4 one, so
804804
* collapsing to that IPv4 is a faithful canonical form: IPv4-compatible ({@code ::d.d.d.d}),
805805
* IPv4-mapped ({@code ::ffff:d.d.d.d}), IPv4-translated ({@code ::ffff:0:d.d.d.d}, RFC 2765),
806-
* NAT64 (RFC 6052 well-known {@code 64:ff9b::/96} and RFC 8215 local-use {@code 64:ff9b:1::/48}),
807-
* and 6to4 (RFC 3056 {@code 2002::/16}).
806+
* NAT64 — the RFC 6052 well-known {@code 64:ff9b::/96} (low 32 bits) and the RFC 8215 local-use
807+
* {@code 64:ff9b:1::/48} (RFC 6052 /48 layout: bytes 6-7 and 9-10) — and 6to4 (RFC 3056
808+
* {@code 2002::/16}).
808809
*/
809810
private static byte[] extractEmbeddedIpv4(byte[] addressBytes) {
810811
if (addressBytes.length != 16) {
811812
return null;
812813
}
813-
if (isIpv4CompatibleOrMapped(addressBytes) || isIpv4Translated(addressBytes) || isNat64(addressBytes)) {
814+
if (isIpv4CompatibleOrMapped(addressBytes)
815+
|| isIpv4Translated(addressBytes)
816+
|| isNat64WellKnown(addressBytes)) {
814817
return Arrays.copyOfRange(addressBytes, 12, 16);
815818
}
819+
if (isNat64LocalUse(addressBytes)) {
820+
// RFC 6052 /48 layout: the 32-bit IPv4 is split across bytes 6-7 and 9-10; byte 8 is
821+
// the reserved u-octet and is skipped. Reading the low 32 bits here (bytes 12-15) would
822+
// misread the address — letting a /48-embedded internal destination through when its
823+
// suffix looks routable, and over-blocking a /48-embedded routable one whose suffix is 0.
824+
return new byte[] {addressBytes[6], addressBytes[7], addressBytes[9], addressBytes[10]};
825+
}
816826
if (isSixToFour(addressBytes)) {
817827
return Arrays.copyOfRange(addressBytes, 2, 6);
818828
}
@@ -851,12 +861,40 @@ private static List<byte[]> embeddedIpv4Candidates(byte[] addressBytes) {
851861
return List.of();
852862
}
853863

854-
/** {@code 64:ff9b::/32} — spans the RFC 6052 well-known and RFC 8215 local-use NAT64 prefixes. */
855-
private static boolean isNat64(byte[] addressBytes) {
864+
/**
865+
* {@code 64:ff9b::/96} — the RFC 6052 well-known NAT64 prefix. Mandated to use the /96 layout,
866+
* so the embedded IPv4 is the low 32 bits (bytes 12-15). Requires bytes 4-11 to be zero;
867+
* without that, a {@code 64:ff9b:1::/48} local-use address would also match and be misread.
868+
*/
869+
private static boolean isNat64WellKnown(byte[] addressBytes) {
870+
if (addressBytes[0] != 0x00
871+
|| addressBytes[1] != 0x64
872+
|| addressBytes[2] != (byte) 0xff
873+
|| addressBytes[3] != (byte) 0x9b) {
874+
return false;
875+
}
876+
for (int i = 4; i < 12; i++) {
877+
if (addressBytes[i] != 0) {
878+
return false;
879+
}
880+
}
881+
return true;
882+
}
883+
884+
/**
885+
* {@code 64:ff9b:1::/48} — the RFC 8215 local-use NAT64 prefix. Per RFC 6052 the /48 layout
886+
* embeds the IPv4 across bytes 6-7 and 9-10 (byte 8 is the reserved u-octet), which
887+
* {@link #extractEmbeddedIpv4(byte[])} reassembles. Other RFC 6052 prefix lengths use a
888+
* Network-Specific Prefix that cannot be recognized from the address alone, so — like 6rd —
889+
* they are out of scope.
890+
*/
891+
private static boolean isNat64LocalUse(byte[] addressBytes) {
856892
return addressBytes[0] == 0x00
857893
&& addressBytes[1] == 0x64
858894
&& addressBytes[2] == (byte) 0xff
859-
&& addressBytes[3] == (byte) 0x9b;
895+
&& addressBytes[3] == (byte) 0x9b
896+
&& addressBytes[4] == 0x00
897+
&& addressBytes[5] == 0x01;
860898
}
861899

862900
/** {@code 2002::/16} — 6to4 carries its IPv4 in bytes 2-5 rather than the low 32 bits. */

app/server/appsmith-interfaces/src/test/java/com/appsmith/util/RestrictedHostFilterTest.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,16 @@ public void isBlockedIpAddressClass_recognizesZeroPaddedNonRoutableLiterals(Stri
215215
"64:ff9b::7f00:1", // 127.0.0.1
216216
"64:ff9b::a9fe:a9fe", // 169.254.169.254
217217
"64:ff9b::e000:1", // 224.0.0.1
218-
// NAT64 local-use prefix (RFC 8215)
219-
"64:ff9b:1::7f00:1", // 127.0.0.1
220-
"64:ff9b:1::a9fe:a9fe", // 169.254.169.254
218+
// NAT64 local-use prefix (RFC 8215, 64:ff9b:1::/48). Per RFC 6052 the embedded
219+
// IPv4 lives in bytes 6-7 and 9-10 (byte 8 is the reserved u-octet), NOT the low
220+
// 32 bits. The first two have zero in those positions, so they embed 0.0.0.0
221+
// (any-local) and block on that; the next two embed a non-routable address in the
222+
// correct /48 position while carrying a *routable* suffix in the low 32 bits —
223+
// reading the low bits (the old behavior) would let these through.
224+
"64:ff9b:1::7f00:1", // /48 positions zero -> 0.0.0.0
225+
"64:ff9b:1::a9fe:a9fe", // /48 positions zero -> 0.0.0.0
226+
"64:ff9b:1:7f00:0:100:808:808", // /48 embeds 127.0.0.1; low bits 8.8.8.8
227+
"64:ff9b:1:a9fe:a9:fe00:808:808", // /48 embeds 169.254.169.254; low bits 8.8.8.8
221228
// 6to4 (RFC 3056) — embedded IPv4 sits in bytes 2-5
222229
"2002:7f00:1::", // 127.0.0.1
223230
"2002:a9fe:a9fe::", // 169.254.169.254
@@ -257,6 +264,10 @@ public void isBlockedIpAddressClass_recognizesIpv6TransitionEmbeddedNonRoutable(
257264
// IPv6-only network this is how legitimate IPv4 destinations are addressed.
258265
"2002:0808:0808::",
259266
"64:ff9b::808:808",
267+
// NAT64 /48 local-use embedding a routable public IPv4 (8.8.8.8) in the correct
268+
// RFC 6052 /48 position. Reading the low 32 bits (the old behavior) saw 0.0.0.0
269+
// and over-blocked this legitimate destination.
270+
"64:ff9b:1:808:8:800::",
260271
})
261272
public void isBlockedIpAddressClass_stillAllowsRoutableNonCanonicalLiterals(String host) {
262273
assertFalse(

0 commit comments

Comments
 (0)