You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by re...@apache.org on 2026/10/23 13:42:43 UTC
(cxf) 01/02: CXF-7986: WebClient.path method overwrites base URL when given absolute URL as input (#3596)
This is an automated email from the ASF dual-hosted git repository.
reta pushed a commit to branch 4.1.x-fixes
in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 5bcf4e65f117a49c5180027ab67b00801f0660c0
Author: Andriy Redko <dr...@gmail.com>
AuthorDate: Fri Oct 23 07:38:22 2026 -0400
CXF-7986: WebClient.path method overwrites base URL when given absolute URL as input (#3596)
* CXF-7986: WebClient.path method overwrites base URL when given absolute URL as input
* Address code review comments
* Address code review comments
* Address code review comments
* Add tests for UriBuilder::toTemplate()
* Address review comments
* Address code review comments
* Address review comments
* Add more test cases
(cherry picked from commit 89758caeb0823fa1c47b6801f61a3971deac040c)
---
.../org/apache/cxf/jaxrs/impl/UriBuilderImpl.java | 51 +++-
.../apache/cxf/jaxrs/impl/UriBuilderImplTest.java | 305 +++++++++++++++++++++
.../org/apache/cxf/jaxrs/client/WebClientTest.java | 25 ++
3 files changed, 371 insertions(+), 10 deletions(-)
diff --git a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java
index c1131863774..7e50ff59cd9 100644
--- a/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java
+++ b/rt/frontend/jaxrs/src/main/java/org/apache/cxf/jaxrs/impl/UriBuilderImpl.java
@@ -103,7 +103,7 @@ public class UriBuilderImpl extends UriBuilder implements Cloneable {
@Override
public URI build(Object... values) throws IllegalArgumentException, UriBuilderException {
- return doBuild(false, true, values);
+ return build(values, true);
}
private static Map<String, Object> getResolvedTemplates(Map<String, Object> rtemplates) {
@@ -185,13 +185,16 @@ public class UriBuilderImpl extends UriBuilder implements Cloneable {
private URI buildURI(boolean fromEncoded, String thePath, String theQuery, String theFragment)
throws URISyntaxException {
+
+ // Always include the leading slash in order to prevent host overrides
+ if ((scheme != null || host != null || userInfo != null)
+ && thePath.length() != 0 && (!thePath.startsWith("/") && needsLeadingSlash(thePath))) {
+ thePath = "/" + thePath;
+ }
+
if (fromEncoded) {
return buildURIFromEncoded(thePath, theQuery, theFragment);
} else if (!isSchemeOpaque()) {
- if ((scheme != null || host != null || userInfo != null)
- && thePath.length() != 0 && !(thePath.startsWith("/") || thePath.startsWith(";"))) {
- thePath = "/" + thePath;
- }
try {
return buildURIFromEncoded(thePath, theQuery, theFragment);
} catch (Exception ex) {
@@ -235,8 +238,8 @@ public class UriBuilderImpl extends UriBuilder implements Cloneable {
b.append(':').append(port);
}
if (thePath != null && thePath.length() > 0) {
- b.append(thePath.startsWith("/") || b.length() == 0 || originalPathEmpty
- ? thePath : '/' + thePath);
+ b.append(thePath.startsWith("/") || b.length() == 0
+ || (originalPathEmpty && !needsLeadingSlash(thePath)) ? thePath : '/' + thePath);
}
if (theQuery != null && theQuery.length() != 0) {
b.append('?').append(theQuery);
@@ -250,6 +253,30 @@ public class UriBuilderImpl extends UriBuilder implements Cloneable {
return b.toString();
}
+ /**
+ * The `;`, `&` and `+` are valid userinfo characters (RFC 3986), so a path
+ * glued to the host that contains `@` is parsed as userinfo.
+ *
+ * URI.create("http://foo.com;@evil.com").getHost() -> "evil.com"
+ * URI.create("http://foo.com&@evil.com").getHost() -> "evil.com"
+ * URI.create("http://foo.com+@evil.com").getHost() -> "evil.com"
+ *
+ * The straightforward encoding of @ -> %40 does not work either:
+ *
+ * URI.create("http://foo.com;%40evil.com").getHost() -> null
+ *
+ * It does not even matter if the userinfo part is already provided:
+ *
+ * URI.create("http://a:b@foo.com;%40evil.com").getHost() -> null
+ *
+ * To workaround the issue, we have to include leading path slash all the time.
+ */
+ private boolean needsLeadingSlash(String path) {
+ return !path.startsWith(";")
+ || path.indexOf('@') >= 0 /* check if @ is present */
+ || path.indexOf("%40") >= 0; /* @ encoded */
+ }
+
private boolean isSchemeOpaque() {
return schemeSpecificPart != null;
}
@@ -262,7 +289,7 @@ public class UriBuilderImpl extends UriBuilder implements Cloneable {
@Override
public URI buildFromMap(Map<String, ?> map) throws IllegalArgumentException,
UriBuilderException {
- return doBuildFromMap(map, false, true);
+ return buildFromMap(map, true);
}
private URI doBuildFromMap(Map<String, ? extends Object> map, boolean fromEncoded,
@@ -548,7 +575,9 @@ public class UriBuilderImpl extends UriBuilder implements Cloneable {
if (path == null) {
throw new IllegalArgumentException("path is null");
}
- if (isAbsoluteUriPath(path)) {
+ // Only set uri when path is absolute and the current URI's is not set
+ // (scheme and host are all null).
+ if (isAbsoluteUriPath(path) && scheme == null && host == null) {
try {
URI uri = URI.create(path);
this.originalPathEmpty = StringUtils.isEmpty(uri.getPath());
@@ -778,7 +807,9 @@ public class UriBuilderImpl extends UriBuilder implements Cloneable {
public UriBuilder replacePath(String path) {
if (path == null) {
clearPathAndMatrix();
- } else if (isAbsoluteUriPath(path)) {
+ } else if (isAbsoluteUriPath(path) && scheme == null && host == null) {
+ // Only set uri when path is absolute and the current URI's is not set
+ // (scheme and host are all null).
clearPathAndMatrix();
uri(URI.create(path));
} else {
diff --git a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java
index a5778ee9293..da70d70a2ca 100644
--- a/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java
+++ b/rt/frontend/jaxrs/src/test/java/org/apache/cxf/jaxrs/impl/UriBuilderImplTest.java
@@ -40,6 +40,9 @@ import org.apache.cxf.jaxrs.utils.JAXRSUtils;
import org.junit.Test;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
@@ -134,7 +137,309 @@ public class UriBuilderImplTest {
assertEquals("/?a=%25&a2=%7B%7D", uri.toString());
}
+ @Test
+ public void testReplaceFullPath() {
+ UriBuilder ub = UriBuilder.fromPath("");
+ URI uri = ub.build();
+ assertEquals("", uri.toString());
+ uri = ub.replacePath("http://foo.com").build();
+ assertEquals("http://foo.com", uri.toString());
+
+ uri = ub.buildFromEncoded();
+ assertEquals("http://foo.com", uri.toString());
+ }
+
+ @Test
+ public void testReplaceFullPathWhenUriIsSet() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.replacePath("http://bar.com/").build();
+ assertThat(uri.toString(), startsWith("http://foo.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), startsWith("http://foo.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+ }
+
+ @Test
+ public void testBuildAndSetFullPath() {
+ UriBuilder ub = UriBuilder.fromPath("");
+ URI uri = ub.build();
+ assertEquals("", uri.toString());
+ uri = ub.path("http://foo.com").build();
+ assertEquals("http://foo.com", uri.toString());
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), startsWith("http://foo.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+ }
+
+ @Test
+ public void testBuildAndSetFullPathWhenUriIsSet() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.path("http://bar.com/").build();
+ assertThat(uri.toString(), startsWith("http://foo.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), startsWith("http://foo.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+ }
+
+ @Test
+ public void testPathDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+
+ uri = ub.path(".org").build();
+ assertThat(uri.toString(), equalTo("http://foo.com/.org"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://foo.com/.org"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+ }
+
+ @Test
+ public void testReplacePathDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.replacePath(".org").build();
+ assertThat(uri.toString(), equalTo("http://foo.com/.org"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://foo.com/.org"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+ }
+
+ @Test
+ public void testEncodedUserInfoAndUriWithMatrix() {
+ // The UriBuilder::fromUri will treat `evil.com` as the host name
+ UriBuilder ub = UriBuilder.fromUri("http://foo.com;%40evil.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com;@evil.com", uri.toString());
+ assertThat(uri.getHost(), equalTo("evil.com"));
+ }
+
+ @Test
+ public void testUserInfoAndUriWithMatrix() {
+ // The UriBuilder::fromUri will treat `evil.com` as the host name
+ UriBuilder ub = UriBuilder.fromUri("http://foo.com;@evil.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com;@evil.com", uri.toString());
+ assertThat(uri.getHost(), equalTo("evil.com"));
+ }
+
+ @Test
+ public void testEncodedUserInfoSeparatorAndPathWithMatrix() {
+ // The UriBuilder::fromPath will treat `evil.com` as the host name
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com;%40evil.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com;@evil.com", uri.toString());
+ assertThat(uri.getHost(), equalTo("evil.com"));
+ }
+
+ @Test
+ public void testUserInfoSeparatorAndPathWithMatrix() {
+ // The UriBuilder::fromPath will treat `evil.com` as the host name
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com;@evil.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com;@evil.com", uri.toString());
+ assertThat(uri.getHost(), equalTo("evil.com"));
+ }
+
+ @Test
+ public void testPathWithUserInfoDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://a:b@foo.com");
+ URI uri = ub.build();
+ assertEquals("http://a:b@foo.com", uri.toString());
+ uri = ub.path(";@evil.com").build();
+ assertThat(uri.toString(), equalTo("http://a:b@foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://a:b@foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ String template = ub.toTemplate();
+ assertThat(template, equalTo("http://a:b@foo.com/;@evil.com"));
+ }
+
+ @Test
+ public void testUriWithUserInfoDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromUri("http://a:b@foo.com");
+ URI uri = ub.build();
+ assertEquals("http://a:b@foo.com", uri.toString());
+ uri = ub.path(";@evil.com").build();
+ assertThat(uri.toString(), equalTo("http://a:b@foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://a:b@foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ String template = ub.toTemplate();
+ assertThat(template, equalTo("http://a:b@foo.com/;@evil.com"));
+ }
+
+ @Test
+ public void testEncodedUserInfoSeparatorAndPathDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.path(";%40evil.com").build();
+ assertThat(uri.toString(), equalTo("http://foo.com/;%40evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://foo.com/;%40evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ String template = ub.toTemplate();
+ assertThat(template, equalTo("http://foo.com/;%40evil.com"));
+ }
+
+ @Test
+ public void testUserInfoSeparatorAndPathDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.path(";@evil.com").build();
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ String template = ub.toTemplate();
+ assertThat(template, equalTo("http://foo.com/;@evil.com"));
+ }
+
+ @Test
+ public void testUserInfoSeparatorAndReplacePathDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.replacePath(";@evil.com").build();
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+ String template = ub.toTemplate();
+ assertThat(template, equalTo("http://foo.com/;@evil.com"));
+ }
+
+ @Test
+ public void testUserInfoSeparatorAndReplaceMatrixDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.replaceMatrix("@evil.com").build();
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ String template = ub.toTemplate();
+ assertThat(template, equalTo("http://foo.com/;@evil.com"));
+ }
+
+ @Test
+ public void testUserInfoSeparatorAndMatrixDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.matrixParam("@evil.com").build();
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ String template = ub.toTemplate();
+ assertThat(template, equalTo("http://foo.com/;@evil.com"));
+ }
+
+ @Test
+ public void testUserInfoSeparatorAndMatrixParamDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.matrixParam("param", "@evil.com").build();
+ assertThat(uri.toString(), equalTo("http://foo.com/;param=@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded();
+ assertThat(uri.toString(), equalTo("http://foo.com/;param=@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ String template = ub.toTemplate();
+ assertThat(template, equalTo("http://foo.com/;param=@evil.com"));
+ }
+
+ @Test
+ public void testUserInfoSeparatorAndTemplatedPathDoesNotChangeHost() {
+ UriBuilder ub = UriBuilder.fromPath("http://foo.com");
+ URI uri = ub.build();
+ assertEquals("http://foo.com", uri.toString());
+ uri = ub.path("{x}").build(";@evil.com");
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+
+ uri = ub.buildFromEncoded(";@evil.com");
+ assertThat(uri.toString(), equalTo("http://foo.com/;@evil.com"));
+ assertThat(uri.getHost(), equalTo("foo.com"));
+ }
+
+ @Test
+ public void testTemplateUriDoesNotChangeHost() {
+ final String template = UriBuilder
+ .fromUri("http://foo.com")
+ .path(".org")
+ .toTemplate();
+ assertEquals("http://foo.com/.org", template);
+ }
+
+ @Test
+ public void testTemplatePathDoesNotChangeHost() {
+ final String template = UriBuilder
+ .fromPath("http://foo.com")
+ .path(".org")
+ .toTemplate();
+ assertEquals("http://foo.com/.org", template);
+ }
+
+ @Test
+ public void testTemplateWithPathDoesNotChangeHost() {
+ final String template = UriBuilder
+ .fromPath("http://foo.com/aaa")
+ .path(".org")
+ .toTemplate();
+ assertEquals("http://foo.com/aaa/.org", template);
+ }
+
+ @Test
+ public void testTemplatePathDoesNotReplaceUri() {
+ final String template = UriBuilder
+ .fromPath("http://foo.com/")
+ .path("http://bar.com")
+ .toTemplate();
+ assertThat(template, startsWith("http://foo.com/"));
+ }
@Test
public void testResolveTemplate() {
diff --git a/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/WebClientTest.java b/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/WebClientTest.java
index e6566de1f51..58db96c53f1 100644
--- a/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/WebClientTest.java
+++ b/rt/rs/client/src/test/java/org/apache/cxf/jaxrs/client/WebClientTest.java
@@ -48,6 +48,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder;
import static org.junit.Assert.assertEquals;
@@ -535,6 +536,30 @@ public class WebClientTest {
assertThat(configuration.getBus().getState(), equalTo(BusState.SHUTDOWN));
}
+ @Test
+ public void pathAllowsAddingURLAsPathElement() {
+ final WebClient webClient = WebClient.create("http://www.foo.com").path("http://www.bar.com");
+ assertThat(webClient.getCurrentURI().toString(), startsWith("http://www.foo.com/"));
+ assertThat(webClient.getCurrentURI().getHost(), equalTo("www.foo.com"));
+ }
+
+ @Test
+ public void preservesPreviouslyAddedPathParameters() {
+ final WebClient webClient = WebClient.create("http://www.foo.com")
+ .path("foo")
+ .path("bar")
+ .path("http://www.bar.com");
+ assertThat(webClient.getCurrentURI().toString(), startsWith("http://www.foo.com/foo/bar/"));
+ assertThat(webClient.getCurrentURI().getHost(), equalTo("www.foo.com"));
+ }
+
+ @Test
+ public void outputOfUriUtilsEncodePathSegmentIsSafe() {
+ final WebClient webClient = WebClient.create("http://www.foo.com").path("http:%2F%2Fwww.bar.com");
+ assertThat(webClient.getCurrentURI().toString(), startsWith("http://www.foo.com/"));
+ assertThat(webClient.getCurrentURI().getHost(), equalTo("www.foo.com"));
+ }
+
private static final class ParamConverterProviderImpl implements ParamConverterProvider {
@SuppressWarnings("unchecked")