This is an automated email from the ASF dual-hosted git repository. robertlazarski pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-core.git
commit 0e256fd0c502dd67e5a7b966a8a0c4d47881c04b Author: Robert Lazarski <[email protected]> AuthorDate: Tue Aug 4 08:40:40 2026 -1000 Bound the response-endpoint name lookup Pre-push Gemini review flagged the DNS lookup in resolvesToRestrictedAddress as a blocking call on the request path, which is right: InetAddress offers no timeout of its own, so a caller could pin threads just by naming hosts that resolve slowly. Two changes. A literal address is now recognised syntactically and never sent to the resolver, which covers the direct-IP cases including the instance-metadata address. A real host name is resolved on a daemon thread the caller can give up on, bounded by responseEndpointResolveTimeoutMillis (default 2000); on timeout the endpoint is refused, since a name that will not resolve promptly here would stall or fail the send anyway. Co-Authored-By: Claude Fable 5 <[email protected]> --- .../addressing/ResponseEndpointPolicy.java | 129 +++++++++++++++++++-- .../addressing/ResponseEndpointPolicyTest.java | 23 ++++ modules/kernel/conf/axis2.xml | 4 + modules/webapp/conf/axis2.xml | 4 + 4 files changed, 151 insertions(+), 9 deletions(-) diff --git a/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java b/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java index c684d36967..377c6befa2 100644 --- a/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java +++ b/modules/addressing/src/org/apache/axis2/handlers/addressing/ResponseEndpointPolicy.java @@ -34,6 +34,14 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Locale; import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * Decides whether a {@code wsa:ReplyTo} or {@code wsa:FaultTo} endpoint @@ -81,6 +89,12 @@ final class ResponseEndpointPolicy { static final String BLOCK_PRIVATE_NETWORKS = "blockPrivateNetworkResponseEndpoints"; static final String ALLOWED_HOSTS = "allowedResponseEndpointHosts"; static final String ALLOWED_SCHEMES_PARAMETER = "allowedResponseEndpointSchemes"; + static final String RESOLVE_TIMEOUT = "responseEndpointResolveTimeoutMillis"; + + /** Long enough for a healthy resolver, short enough not to pin a thread. */ + static final long DEFAULT_RESOLVE_TIMEOUT_MILLIS = 2000L; + + private static ExecutorService resolver; /** * Schemes Axis2 ships a sender for that can carry a decoupled reply. @@ -171,7 +185,8 @@ final class ResponseEndpointPolicy { } return !resolvesToRestrictedAddress(host, - booleanParameter(messageContext, BLOCK_PRIVATE_NETWORKS, false)); + booleanParameter(messageContext, BLOCK_PRIVATE_NETWORKS, false), + longParameter(messageContext, RESOLVE_TIMEOUT, DEFAULT_RESOLVE_TIMEOUT_MILLIS)); } /** @@ -186,15 +201,25 @@ final class ResponseEndpointPolicy { * the check still removes the direct-IP and static-name cases that make this * reachable in practice. */ - private static boolean resolvesToRestrictedAddress(String host, boolean blockPrivateNetworks) { + private static boolean resolvesToRestrictedAddress(String host, boolean blockPrivateNetworks, + long resolveTimeoutMillis) { InetAddress[] addresses; - try { - addresses = InetAddress.getAllByName(host); - } catch (UnknownHostException e) { - // Unresolvable here means the send would fail anyway, so refuse - // rather than pass an unknown destination through. - log.warn("Rejecting WS-Addressing response endpoint with unresolvable host"); - return true; + if (isIpLiteral(host)) { + // No name to look up, so the common cases — a direct IP, including + // the instance-metadata address — cost nothing. + try { + addresses = new InetAddress[] { InetAddress.getByName(host) }; + } catch (UnknownHostException e) { + log.warn("Rejecting WS-Addressing response endpoint with unparseable address"); + return true; + } + } else { + addresses = resolveWithTimeout(host, resolveTimeoutMillis); + if (addresses == null) { + // Unresolved here means the send would fail or hang anyway, so + // refuse rather than pass an unknown destination through. + return true; + } } for (int i = 0; i < addresses.length; i++) { InetAddress address = addresses[i]; @@ -229,6 +254,76 @@ final class ResponseEndpointPolicy { return false; } + /** + * Whether the host is already a literal address, so no name lookup is needed. + * Deliberately syntactic — anything with only hex digits, dots, colons and + * brackets cannot be a registered name. + */ + private static boolean isIpLiteral(String host) { + for (int i = 0; i < host.length(); i++) { + char c = host.charAt(i); + boolean literalChar = (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') + || c == '.' || c == ':' || c == '[' || c == ']' || c == '%'; + if (!literalChar) { + return false; + } + } + return host.indexOf(':') >= 0 || host.indexOf('.') >= 0; + } + + /** + * Resolve a host name without letting a slow or unresponsive DNS server hold + * the request thread. + * + * <p>The lookup runs on a separate thread so the caller can give up on it. + * {@code InetAddress} exposes no timeout of its own, and this runs on the + * request path, so without a bound a caller could pin threads simply by + * naming hosts that resolve slowly. + * + * @return the resolved addresses, or null if the name did not resolve in time + */ + private static InetAddress[] resolveWithTimeout(final String host, long timeoutMillis) { + Future<InetAddress[]> pending = resolver().submit(new Callable<InetAddress[]>() { + public InetAddress[] call() throws UnknownHostException { + return InetAddress.getAllByName(host); + } + }); + try { + return pending.get(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + pending.cancel(true); + log.warn("Rejecting WS-Addressing response endpoint: host did not resolve within " + + timeoutMillis + "ms"); + return null; + } catch (ExecutionException e) { + log.warn("Rejecting WS-Addressing response endpoint with unresolvable host"); + return null; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + pending.cancel(true); + return null; + } + } + + /** + * Lookup threads are daemons and the pool is unbounded-but-caching, so idle + * threads retire on their own and none of this outlives the JVM or holds up + * a redeployment. + */ + private static synchronized ExecutorService resolver() { + if (resolver == null) { + resolver = Executors.newCachedThreadPool(new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "axis2-wsa-endpoint-resolver"); + t.setDaemon(true); + return t; + } + }); + } + return resolver; + } + /** IPv6 unique local addresses, fc00::/7, which isSiteLocalAddress misses. */ private static boolean isUniqueLocalIPv6(InetAddress address) { byte[] bytes = address.getAddress(); @@ -263,6 +358,22 @@ final class ResponseEndpointPolicy { return schemes; } + private static long longParameter(MessageContext messageContext, String name, + long defaultValue) { + String value = stringParameter(messageContext, name); + if (value == null || value.trim().isEmpty()) { + return defaultValue; + } + try { + long parsed = Long.parseLong(value.trim()); + return parsed > 0 ? parsed : defaultValue; + } catch (NumberFormatException e) { + log.warn("Ignoring non-numeric value '" + value + "' for parameter '" + name + + "'; using the default of " + defaultValue + "ms"); + return defaultValue; + } + } + private static boolean booleanParameter(MessageContext messageContext, String name, boolean defaultValue) { if (messageContext == null) { diff --git a/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java b/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java index c8f3023a74..0d1af45aa8 100644 --- a/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java +++ b/modules/addressing/test/org/apache/axis2/handlers/addressing/ResponseEndpointPolicyTest.java @@ -204,6 +204,29 @@ public class ResponseEndpointPolicyTest extends TestCase { new EndpointReference("http://127.0.0.1:8080/sink"), messageContext)); } + /** + * A name that cannot resolve is refused rather than passed through, and the + * lookup is bounded so a slow resolver cannot hold the request thread. The + * .invalid TLD is reserved by RFC 2606 precisely so it never resolves. + */ + public void testUnresolvableHostIsRejected() { + assertFalse(ResponseEndpointPolicy.isAllowed( + new EndpointReference("http://no-such-host.invalid/replies"), messageContext)); + } + + /** + * A literal address needs no name lookup at all, so the check costs nothing + * on the request path for the direct-IP cases. + */ + public void testLiteralAddressNeedsNoResolution() throws Exception { + setParameter(ResponseEndpointPolicy.RESOLVE_TIMEOUT, "1"); + // Would time out if this went to the resolver; it must not. + assertFalse(ResponseEndpointPolicy.isAllowed( + new EndpointReference("http://169.254.169.254/latest/meta-data/"), messageContext)); + assertTrue(ResponseEndpointPolicy.isAllowed( + new EndpointReference("http://192.0.2.25/replies"), messageContext)); + } + public void testMalformedAddressIsRejected() { assertFalse(ResponseEndpointPolicy.isAllowed( new EndpointReference("http://[not a uri"), messageContext)); diff --git a/modules/kernel/conf/axis2.xml b/modules/kernel/conf/axis2.xml index 0e04fc48c4..1c474c016e 100644 --- a/modules/kernel/conf/axis2.xml +++ b/modules/kernel/conf/axis2.xml @@ -79,6 +79,10 @@ allowedResponseEndpointHosts, when set to a comma-separated host list, restricts response endpoints to exactly those hosts. + responseEndpointResolveTimeoutMillis (default 2000) bounds the name lookup + used by the checks above, so a slow resolver cannot hold a request thread. + Literal addresses are never looked up. + allowedResponseEndpointSchemes defaults to http,https,jms,mailto,tcp - a decoupled reply over JMS, mail or TCP is a legitimate configuration, so the list is not restricted to HTTP. Schemes outside it, such as file or gopher, diff --git a/modules/webapp/conf/axis2.xml b/modules/webapp/conf/axis2.xml index 80ecc6753b..a26c7723fe 100644 --- a/modules/webapp/conf/axis2.xml +++ b/modules/webapp/conf/axis2.xml @@ -79,6 +79,10 @@ allowedResponseEndpointHosts, when set to a comma-separated host list, restricts response endpoints to exactly those hosts. + responseEndpointResolveTimeoutMillis (default 2000) bounds the name lookup + used by the checks above, so a slow resolver cannot hold a request thread. + Literal addresses are never looked up. + allowedResponseEndpointSchemes defaults to http,https,jms,mailto,tcp - a decoupled reply over JMS, mail or TCP is a legitimate configuration, so the list is not restricted to HTTP. Schemes outside it, such as file or gopher,
