You are viewing a plain text version of this content. The canonical link for it is here.
Posted to java-commits@axis.apache.org by ro...@apache.org on 2026/08/06 13:38:16 UTC
(axis-axis2-java-core) 10/17: Bound the response-endpoint name lookup
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 <ro...@gmail.com>
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 <no...@anthropic.com>
---
.../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,