From 0ade36e2ee72aae917b76dc8939dac6e9c961a2f Mon Sep 17 00:00:00 2001 From: Zibran Khan Date: Mon, 2 Nov 2026 20:42:53 +0530 Subject: [PATCH] scope proxy credentials to the proxy in HTTPProxyConfigurator The proxy username and password were registered under AuthScope(null, -1), which HttpClient also consults for an origin server's 401 challenge, so a server reached through the proxy could collect the proxy password with a Basic challenge. Scope the credentials to the proxy host and port, and add them to a credentials provider the request already carries instead of replacing it, so origin credentials set through enableAuthentication survive. --- .../http/HTTPProxyConfiguratorTest.java | 5 +- .../httpclient5/HTTPProxyConfigurator.java | 17 +- .../httpclient5/ProxyCredentialScopeTest.java | 165 ++++++++++++++++++ 3 files changed, 182 insertions(+), 5 deletions(-) create mode 100644 modules/transport/http/src/test/java/org/apache/axis2/transport/http/impl/httpclient5/ProxyCredentialScopeTest.java diff --git a/modules/integration/test/org/apache/axis2/transport/http/HTTPProxyConfiguratorTest.java b/modules/integration/test/org/apache/axis2/transport/http/HTTPProxyConfiguratorTest.java index f870ff316e..50b2eb03af 100644 --- a/modules/integration/test/org/apache/axis2/transport/http/HTTPProxyConfiguratorTest.java +++ b/modules/integration/test/org/apache/axis2/transport/http/HTTPProxyConfiguratorTest.java @@ -37,6 +37,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import org.apache.axis2.transport.http.impl.httpclient5.HTTPProxyConfigurator; @@ -93,10 +94,12 @@ public void testProxyWithCredentials() throws AxisFault { final CredentialsProvider provider = clientContext.getCredentialsProvider(); assertNotNull(provider); - final Credentials credentials = provider.getCredentials(new AuthScope(null, -1), clientContext); + final Credentials credentials = provider.getCredentials(new AuthScope(hostname, portNumber), clientContext); assertNotNull(credentials); assertEquals(user, credentials.getUserPrincipal().getName()); assertEquals(pass, new String(credentials.getPassword())); + // The origin server's challenge must not be answered with the proxy credentials + assertNull(provider.getCredentials(new AuthScope("origin.example", 443), clientContext)); } @Test diff --git a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/impl/httpclient5/HTTPProxyConfigurator.java b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/impl/httpclient5/HTTPProxyConfigurator.java index 149cf0b74c..cddab45817 100644 --- a/modules/transport/http/src/main/java/org/apache/axis2/transport/http/impl/httpclient5/HTTPProxyConfigurator.java +++ b/modules/transport/http/src/main/java/org/apache/axis2/transport/http/impl/httpclient5/HTTPProxyConfigurator.java @@ -146,11 +146,20 @@ public static void configure(MessageContext messageContext, RequestConfig.Builde // AXIS2-6051, CredentialsProvider no longer has setCredentials() however BasicCredentialsProvider // does have it. clientContext.getCredentialsProvider() returns CredentialsProvider. - if (proxyCredentials != null) { + if (proxyCredentials != null && proxyHost != null) { requestConfig.setAuthenticationEnabled(true); - BasicCredentialsProvider credsProvider = new BasicCredentialsProvider(); - clientContext.setCredentialsProvider(credsProvider); - credsProvider.setCredentials(new AuthScope(null, -1), proxyCredentials); + // The same provider answers the origin server's challenges, so the + // credentials are scoped to the proxy rather than to any host, and a + // provider already holding the origin's credentials is kept. + CredentialsProvider existingProvider = clientContext.getCredentialsProvider(); + BasicCredentialsProvider credsProvider; + if (existingProvider instanceof BasicCredentialsProvider) { + credsProvider = (BasicCredentialsProvider) existingProvider; + } else { + credsProvider = new BasicCredentialsProvider(); + clientContext.setCredentialsProvider(credsProvider); + } + credsProvider.setCredentials(new AuthScope(proxyHost, proxyPort), proxyCredentials); } if (proxyHost != null && proxyPort > 0) { HttpHost proxy = new HttpHost(proxyHost, proxyPort); diff --git a/modules/transport/http/src/test/java/org/apache/axis2/transport/http/impl/httpclient5/ProxyCredentialScopeTest.java b/modules/transport/http/src/test/java/org/apache/axis2/transport/http/impl/httpclient5/ProxyCredentialScopeTest.java new file mode 100644 index 0000000000..fc20fcfec1 --- /dev/null +++ b/modules/transport/http/src/test/java/org/apache/axis2/transport/http/impl/httpclient5/ProxyCredentialScopeTest.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.axis2.transport.http.impl.httpclient5; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import junit.framework.TestCase; +import org.apache.axis2.context.ConfigurationContext; +import org.apache.axis2.context.MessageContext; +import org.apache.axis2.engine.AxisConfiguration; +import org.apache.axis2.kernel.http.HTTPConstants; +import org.apache.axis2.transport.http.HttpTransportProperties; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Proxy credentials must answer the proxy's challenge only. + * + *

The client sends the same credentials provider to the proxy and to the + * origin server. Credentials registered for any host would therefore also be + * sent in reply to a 401 from the origin, which hands the proxy password to + * whatever server the request happens to reach. + */ +public class ProxyCredentialScopeTest extends TestCase { + + private static final String PROXY_USER = "proxyuser"; + private static final String PROXY_PASSWORD = "proxypass"; + private static final String ORIGIN_USER = "originuser"; + private static final String ORIGIN_PASSWORD = "originpass"; + + private HttpServer proxy; + private int proxyPort; + private final AtomicReference originAuthorization = new AtomicReference(); + private final AtomicReference proxyAuthorization = new AtomicReference(); + + protected void setUp() throws Exception { + super.setUp(); + originAuthorization.set(null); + proxyAuthorization.set(null); + proxy = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + proxyPort = proxy.getAddress().getPort(); + + // Stands in for the proxy and, behind it, the origin server. Requests + // arrive in absolute form; the path says which of the two challenges. + proxy.createContext("/", new HttpHandler() { + public void handle(HttpExchange exchange) throws IOException { + exchange.getRequestBody().readAllBytes(); + String path = exchange.getRequestURI().getPath(); + if ("/origin-challenge".equals(path)) { + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + if (auth == null) { + exchange.getResponseHeaders().add("WWW-Authenticate", + "Basic realm=\"origin\""); + exchange.sendResponseHeaders(401, -1); + } else { + originAuthorization.set(auth); + exchange.sendResponseHeaders(200, -1); + } + } else if ("/proxy-challenge".equals(path)) { + String auth = exchange.getRequestHeaders().getFirst("Proxy-Authorization"); + if (auth == null) { + exchange.getResponseHeaders().add("Proxy-Authenticate", + "Basic realm=\"proxy\""); + exchange.sendResponseHeaders(407, -1); + } else { + proxyAuthorization.set(auth); + exchange.sendResponseHeaders(200, -1); + } + } else { + exchange.sendResponseHeaders(404, -1); + } + exchange.close(); + } + }); + proxy.start(); + } + + protected void tearDown() throws Exception { + proxy.stop(0); + super.tearDown(); + } + + /** The case that matters: a 401 from the origin must not be answered with the proxy password. */ + public void testOriginChallengeDoesNotReceiveProxyCredentials() throws Exception { + RequestImpl request = execute(newContext(), null, "/origin-challenge"); + assertEquals(401, request.getStatusCode()); + assertNull("The proxy credentials were sent to the origin server", + originAuthorization.get()); + } + + /** The control: the proxy's own challenge is still answered. */ + public void testProxyChallengeReceivesProxyCredentials() throws Exception { + RequestImpl request = execute(newContext(), null, "/proxy-challenge"); + assertEquals(200, request.getStatusCode()); + assertEquals(basic(PROXY_USER, PROXY_PASSWORD), proxyAuthorization.get()); + } + + /** Credentials configured for the origin are kept when the proxy is configured on top. */ + public void testOriginCredentialsSurviveProxyConfiguration() throws Exception { + HttpTransportPropertiesImpl.Authenticator authenticator = + new HttpTransportPropertiesImpl.Authenticator(); + authenticator.setUsername(ORIGIN_USER); + authenticator.setPassword(ORIGIN_PASSWORD); + authenticator.setHost("origin.example"); + RequestImpl request = execute(newContext(), authenticator, "/origin-challenge"); + assertEquals(200, request.getStatusCode()); + assertEquals(basic(ORIGIN_USER, ORIGIN_PASSWORD), originAuthorization.get()); + } + + private MessageContext newContext() throws Exception { + ConfigurationContext cc = new ConfigurationContext(new AxisConfiguration()); + MessageContext mc = cc.createMessageContext(); + HttpTransportProperties.ProxyProperties proxyProperties = + new HttpTransportProperties.ProxyProperties(); + proxyProperties.setProxyName("127.0.0.1"); + proxyProperties.setProxyPort(proxyPort); + proxyProperties.setUserName(PROXY_USER); + proxyProperties.setPassWord(PROXY_PASSWORD); + mc.setProperty(HTTPConstants.PROXY, proxyProperties); + return mc; + } + + private RequestImpl execute(MessageContext mc, HttpTransportPropertiesImpl.Authenticator authenticator, + String path) throws Exception { + HttpClient client = HttpClientBuilder.create().build(); + RequestImpl request = new RequestImpl(client, mc, "GET", + URI.create("http://origin.example" + path), null); + if (authenticator != null) { + request.enableAuthentication(authenticator); + } + request.execute(); + request.releaseConnection(); + return request; + } + + private static String basic(String user, String password) { + return "Basic " + Base64.getEncoder().encodeToString( + (user + ":" + password).getBytes(StandardCharsets.UTF_8)); + } +}