You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by be...@apache.org on 2026/10/10 12:52:22 UTC
(tapestry-5) 01/04: TAP5-2832: LocalhostOnly detect IPv6 correctly (#64)
This is an automated email from the ASF dual-hosted git repository.
benweidig pushed a commit to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit d2dc82479a17d6134eab13d4e5e18251e90878d1
Author: Ben Weidig <be...@users.noreply.github.com>
AuthorDate: Sat Oct 10 12:30:50 2026 +0200
TAP5-2832: LocalhostOnly detect IPv6 correctly (#64)
LocalhostOnly didn't detect a few of the IPv6 variants.
Furthermore, it might trigger a DNS lookup, which could impact performance.
Therefore, it now relies on HttpServletRequest.getRemoteAddr (via Request).
---
.../internal/services/security/LocalhostOnly.java | 86 ++++++++++++++++-
.../internal/test/TestableRequestImpl.java | 10 ++
.../tapestry5/services/DelegatingRequest.java | 5 +
.../services/security/LocalhostOnlyTest.java | 104 +++++++++++++++++++++
.../http/internal/services/RequestImpl.java | 7 +-
.../apache/tapestry5/http/services/Request.java | 11 +++
6 files changed, 218 insertions(+), 5 deletions(-)
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/security/LocalhostOnly.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/security/LocalhostOnly.java
index e5c2871f6..1566ccf2f 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/security/LocalhostOnly.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/services/security/LocalhostOnly.java
@@ -1,4 +1,4 @@
-// Copyright 2011, 2012 The Apache Software Foundation
+// Copyright 2011, 2012, 2026 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -17,17 +17,95 @@ package org.apache.tapestry5.internal.services.security;
import org.apache.tapestry5.http.services.Request;
import org.apache.tapestry5.services.security.WhitelistAnalyzer;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
/**
- * Standard analyzer that places requests from the "localhost", "127.0.0.1", "0:0:0:0:0:0:0:1%0", or :"0:0:0:0:0:0:0:1" onto the white list.
+ * Standard analyzer that places requests from loopback addresses onto the whitelist.
+ * Recognized forms include IPv4 (127.x.x.x), IPv6 (::1, 0:0:0:0:0:0:0:1), compressed
+ * or with arbitrary scope IDs (e.g. ::1%lo, 0:0:0:0:0:0:0:1%eth0), and IPv4-mapped
+ * IPv6 loopback (::ffff:127.0.0.1).
*
* @since 5.3
*/
public class LocalhostOnly implements WhitelistAnalyzer
{
+ // Common loopback address literals that need no parsing.
+ // Adding "localhost" avoids a potential hostname lookup
+ // via InetAddress.getByName().
+ private static final Set<String> LOOPBACK_LITERALS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+ "localhost",
+ "127.0.0.1",
+ "::1",
+ "0:0:0:0:0:0:0:1"
+ )));
+
public boolean isRequestOnWhitelist(Request request)
{
- String remoteHost = request.getRemoteHost();
+ String remoteAddr = request.getRemoteAddr();
+
+ // Fastpath
+ if (LOOPBACK_LITERALS.contains(remoteAddr))
+ {
+ return true;
+ }
+
+ // Strip IPv6 scope ID (e.g. ::1%lo, 0:0:0:0:0:0:0:1%eth0) before parsing
+ int percentIdx = remoteAddr.indexOf('%');
+ String addr = (percentIdx >= 0) ? remoteAddr.substring(0, percentIdx) : remoteAddr;
+
+ try
+ {
+ InetAddress inetAddress = InetAddress.getByName(addr);
+
+ if (inetAddress.isLoopbackAddress())
+ return true;
+
+ // InetAddress.isLoopbackAddress() returns false for IPv4-mapped IPv6 loopback
+ // (e.g. ::ffff:127.0.0.1). Extract the embedded IPv4 address and check that instead.
+ if (inetAddress instanceof Inet6Address)
+ {
+ byte[] bytes = inetAddress.getAddress();
+ if (isIPv4MappedAddress(bytes))
+ {
+ InetAddress embedded = InetAddress.getByAddress(new byte[]{ bytes[12], bytes[13], bytes[14], bytes[15] });
+ return embedded.isLoopbackAddress();
+ }
+ }
+
+ return false;
+ }
+ catch (UnknownHostException e)
+ {
+ return false;
+ }
+ }
+
+ // IPv4-mapped IPv6 addresses have the form ::ffff:a.b.c.d, encoded as 16 bytes:
+ // - bytes 0–9: all zero
+ // - bytes 10–11: 0xff 0xff (the "mapped" marker)
+ // - bytes 12–15: the IPv4 address
+ private static boolean isIPv4MappedAddress(byte[] bytes)
+ {
+ if (bytes.length != 16)
+ {
+ return false;
+ }
+
+ // Bytes 0–9 must all be zero
+ for (int i = 0; i < 10; i++)
+ {
+ if (bytes[i] != 0) {
+ return false;
+ }
+ }
- return remoteHost.equals("localhost") || remoteHost.equals("127.0.0.1") || remoteHost.equals("0:0:0:0:0:0:0:1%0") || remoteHost.equals("0:0:0:0:0:0:0:1");
+ // Bytes 10–11 must be 0xff 0xff
+ return bytes[10] == (byte) 0xff && bytes[11] == (byte) 0xff;
}
}
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/internal/test/TestableRequestImpl.java b/tapestry-core/src/main/java/org/apache/tapestry5/internal/test/TestableRequestImpl.java
index 2e75ba345..cc4134cc8 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/internal/test/TestableRequestImpl.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/internal/test/TestableRequestImpl.java
@@ -263,6 +263,16 @@ public class TestableRequestImpl implements TestableRequest
return "localhost";
}
+ /**
+ * Always returns "127.0.0.1".
+ *
+ * @since 5.10
+ */
+ public String getRemoteAddr()
+ {
+ return "127.0.0.1";
+ }
+
public boolean isSessionInvalidated()
{
return false;
diff --git a/tapestry-core/src/main/java/org/apache/tapestry5/services/DelegatingRequest.java b/tapestry-core/src/main/java/org/apache/tapestry5/services/DelegatingRequest.java
index db4c2b008..7578867dd 100644
--- a/tapestry-core/src/main/java/org/apache/tapestry5/services/DelegatingRequest.java
+++ b/tapestry-core/src/main/java/org/apache/tapestry5/services/DelegatingRequest.java
@@ -165,6 +165,11 @@ public class DelegatingRequest implements Request
return request.getRemoteHost();
}
+ public String getRemoteAddr()
+ {
+ return request.getRemoteAddr();
+ }
+
public boolean isSessionInvalidated()
{
return request.isSessionInvalidated();
diff --git a/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/security/LocalhostOnlyTest.java b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/security/LocalhostOnlyTest.java
new file mode 100644
index 000000000..a617f59e0
--- /dev/null
+++ b/tapestry-core/src/test/java/org/apache/tapestry5/internal/services/security/LocalhostOnlyTest.java
@@ -0,0 +1,104 @@
+// Copyright 2026 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.tapestry5.internal.services.security;
+
+import org.apache.tapestry5.http.services.Request;
+import org.easymock.EasyMock;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+// TAP5-2832
+class LocalhostOnlyTest
+{
+ static Stream<String> whitelist_addresses()
+ {
+ return Stream.of(
+ // Standard loopback
+ "localhost",
+ "127.0.0.1",
+
+ // Full-form IPv6 loopback
+ "0:0:0:0:0:0:0:1",
+ "0:0:0:0:0:0:0:1%0",
+
+ // Compressed IPv6 loopback
+ "::1",
+
+ // IPv6 loopback with arbitrary scope IDs (OS-dependent: Linux uses %lo, Windows uses numeric IDs)
+ "::1%lo",
+ "::1%eth0",
+ "::1%12",
+ "0:0:0:0:0:0:0:1%lo",
+ "0:0:0:0:0:0:0:1%eth0",
+
+ // IPv4-mapped IPv6 loopback (dual-stack environments)
+ "::ffff:127.0.0.1",
+ "::ffff:7f00:1"
+ );
+ }
+
+ private Request mockRequest(String remoteAddr)
+ {
+ Request request = EasyMock.mock(Request.class);
+ EasyMock.expect(request.getRemoteAddr()).andReturn(remoteAddr);
+ EasyMock.replay(request);
+ return request;
+ }
+
+ @ParameterizedTest
+ @MethodSource("whitelist_addresses")
+ void localhost_address_is_on_whitelist(String remoteAddr)
+ {
+ // ARRANGE
+ Request request = mockRequest(remoteAddr);
+
+ // ACT
+ boolean result = new LocalhostOnly().isRequestOnWhitelist(request);
+
+ // ASSERT
+ assertTrue(result, "Expected '" + remoteAddr + "' to be on the whitelist");
+ }
+
+ static Stream<String> non_whitelist_addresses()
+ {
+ return Stream.of(
+ "192.168.1.1",
+ "10.0.0.1",
+ "172.16.0.1",
+ "remotehost.example.com",
+ "2001:db8::1",
+ "fe80::1",
+ "::2"
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("non_whitelist_addresses")
+ void non_localhost_address_is_not_on_whitelist(String remoteAddr)
+ {
+ // ARRANGE
+ Request request = mockRequest(remoteAddr);
+
+ // ACT
+ boolean result = new LocalhostOnly().isRequestOnWhitelist(request);
+
+ // ASSERT
+ assertFalse(result, "Expected '" + remoteAddr + "' to be NOT on the whitelist");
+ }
+}
diff --git a/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/RequestImpl.java b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/RequestImpl.java
index 6694a14b3..365301aa5 100644
--- a/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/RequestImpl.java
+++ b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/RequestImpl.java
@@ -234,7 +234,12 @@ public class RequestImpl implements Request
{
return request.getRemoteHost();
}
-
+
+ public String getRemoteAddr()
+ {
+ return request.getRemoteAddr();
+ }
+
/**
* Converts an enumeration (of Strings) into a sorted list of Strings.
*/
diff --git a/tapestry-http/src/main/java/org/apache/tapestry5/http/services/Request.java b/tapestry-http/src/main/java/org/apache/tapestry5/http/services/Request.java
index aaea71111..4d436e81a 100644
--- a/tapestry-http/src/main/java/org/apache/tapestry5/http/services/Request.java
+++ b/tapestry-http/src/main/java/org/apache/tapestry5/http/services/Request.java
@@ -208,6 +208,17 @@ public interface Request
*/
String getRemoteHost();
+ /**
+ * Returns the IP address of the client or last proxy that sent the request,
+ * always as a numeric address string (never a hostname).
+ *
+ * @return a <code>String</code> containing the IP address of the client that sent the request
+ * @since 5.10.0
+ */
+ default String getRemoteAddr() {
+ return null;
+ }
+
/**
* Returns true if the request specified a session, and that session has been invalidated.
*