001// Copyright 2011, 2012, 2026 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.internal.services.security; 016 017import org.apache.tapestry5.http.services.Request; 018import org.apache.tapestry5.services.security.WhitelistAnalyzer; 019 020import java.net.Inet6Address; 021import java.net.InetAddress; 022import java.net.UnknownHostException; 023import java.util.Set; 024 025/** 026 * Standard analyzer that places requests from loopback addresses onto the whitelist. 027 * Recognized forms include IPv4 (127.x.x.x), IPv6 (::1, 0:0:0:0:0:0:0:1), compressed 028 * or with arbitrary scope IDs (e.g. ::1%lo, 0:0:0:0:0:0:0:1%eth0), and IPv4-mapped 029 * IPv6 loopback (::ffff:127.0.0.1). 030 * 031 * @since 5.3 032 */ 033public class LocalhostOnly implements WhitelistAnalyzer 034{ 035 // Common loopback address literals that need no parsing. 036 // Adding "localhost" avoids a potential hostname lookup 037 // via InetAddress.getByName(). 038 private static final Set<String> LOOPBACK_LITERALS = Set.of( 039 "localhost", 040 "127.0.0.1", 041 "::1", 042 "0:0:0:0:0:0:0:1" 043 ); 044 045 public boolean isRequestOnWhitelist(Request request) 046 { 047 String remoteAddr = request.getRemoteAddr(); 048 049 // Fastpath 050 if (LOOPBACK_LITERALS.contains(remoteAddr)) 051 { 052 return true; 053 } 054 055 // Strip IPv6 scope ID (e.g. ::1%lo, 0:0:0:0:0:0:0:1%eth0) before parsing 056 int percentIdx = remoteAddr.indexOf('%'); 057 String addr = (percentIdx >= 0) ? remoteAddr.substring(0, percentIdx) : remoteAddr; 058 059 try 060 { 061 InetAddress inetAddress = InetAddress.getByName(addr); 062 063 if (inetAddress.isLoopbackAddress()) 064 return true; 065 066 // InetAddress.isLoopbackAddress() returns false for IPv4-mapped IPv6 loopback 067 // (e.g. ::ffff:127.0.0.1). Extract the embedded IPv4 address and check that instead. 068 if (inetAddress instanceof Inet6Address) 069 { 070 byte[] bytes = inetAddress.getAddress(); 071 if (isIPv4MappedAddress(bytes)) 072 { 073 InetAddress embedded = InetAddress.getByAddress(new byte[]{ bytes[12], bytes[13], bytes[14], bytes[15] }); 074 return embedded.isLoopbackAddress(); 075 } 076 } 077 078 return false; 079 } 080 catch (UnknownHostException e) 081 { 082 return false; 083 } 084 } 085 086 // IPv4-mapped IPv6 addresses have the form ::ffff:a.b.c.d, encoded as 16 bytes: 087 // - bytes 0–9: all zero 088 // - bytes 10–11: 0xff 0xff (the "mapped" marker) 089 // - bytes 12–15: the IPv4 address 090 private static boolean isIPv4MappedAddress(byte[] bytes) 091 { 092 if (bytes.length != 16) 093 { 094 return false; 095 } 096 097 // Bytes 0–9 must all be zero 098 for (int i = 0; i < 10; i++) 099 { 100 if (bytes[i] != 0) { 101 return false; 102 } 103 } 104 105 // Bytes 10–11 must be 0xff 0xff 106 return bytes[10] == (byte) 0xff && bytes[11] == (byte) 0xff; 107 } 108}