From 07a05ad39ffe924bb17acd8b680848fba7c21275 Mon Sep 17 00:00:00 2001 From: mohammed adib Date: Sun, 19 Jul 2026 12:24:27 +0530 Subject: [PATCH] strip userinfo from the absolute-form proxy request target --- .../netty/request/NettyRequestFactory.java | 5 +- .../java/org/asynchttpclient/uri/Uri.java | 28 ++++++++ .../request/ProxyRequestUriUserInfoTest.java | 70 +++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/netty/request/ProxyRequestUriUserInfoTest.java diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java index b2f23ecbf..c85b60504 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java @@ -336,8 +336,9 @@ private static String requestUri(Uri uri, ProxyServer proxyServer, boolean conne return uri.getAuthority(); } else if (proxyServer != null && !uri.isSecured() && proxyServer.getProxyType().isHttp()) { - // proxy over HTTP, need full url - return uri.toUrl(); + // proxy over HTTP, need full url, minus the userinfo: this request line is sent to the proxy in + // the clear and RFC 9110 §4.2.4 forbids userinfo in a generated request target + return uri.toUrlWithoutUserInfo(); } else { // direct connection to target host or tunnel already connected: only path and query diff --git a/client/src/main/java/org/asynchttpclient/uri/Uri.java b/client/src/main/java/org/asynchttpclient/uri/Uri.java index d7f42f6fe..1f93f69c0 100644 --- a/client/src/main/java/org/asynchttpclient/uri/Uri.java +++ b/client/src/main/java/org/asynchttpclient/uri/Uri.java @@ -145,6 +145,34 @@ public String toUrl() { return url; } + /** + * Same as {@link #toUrl()} but without the deprecated userinfo subcomponent. RFC 9110 §4.2.4: a sender + * MUST NOT generate the userinfo subcomponent (and its "@" delimiter) when an origin-form or + * absolute-form URI reference is generated for a request target, so this is what belongs on the wire + * when an absolute-form target is required. {@link #toUrl()} keeps the userinfo for the caller-visible + * URL. + * + * @return [scheme]://[hostname](:[port])/path(?[query]) + */ + public String toUrlWithoutUserInfo() { + if (userInfo == null) { + return toUrl(); + } + + StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder(); + sb.append(scheme).append("://").append(host); + if (port != -1) { + sb.append(':').append(port); + } + if (path != null) { + sb.append(path); + } + if (query != null) { + sb.append('?').append(query); + } + return sb.toString(); + } + /** * @return [scheme]://[hostname](:[port])/path. Port is omitted if it matches the scheme's default one. */ diff --git a/client/src/test/java/org/asynchttpclient/netty/request/ProxyRequestUriUserInfoTest.java b/client/src/test/java/org/asynchttpclient/netty/request/ProxyRequestUriUserInfoTest.java new file mode 100644 index 000000000..4184a429c --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/ProxyRequestUriUserInfoTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * Licensed 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.asynchttpclient.netty.request; + +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.junit.jupiter.api.Test; + +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +/** + * A plaintext request routed through an HTTP proxy carries an absolute-form request target, which the proxy + * sees and logs in the clear. RFC 9110 §4.2.4 forbids the userinfo subcomponent in a generated request + * target, so the URL credentials must not appear on that request line. + */ +public class ProxyRequestUriUserInfoTest { + + private static NettyRequestFactory factory() { + return new NettyRequestFactory(config().build()); + } + + @Test + public void proxiedRequestTargetDropsUserInfo() { + Request request = get("http://user:secret@origin.example.com/resource?a=b").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest proxied = factory().newNettyRequest(request, false, proxy, null, null); + String requestTarget = proxied.getHttpRequest().uri(); + + assertFalse(requestTarget.contains("secret"), + "the absolute-form request target must not expose the URL credentials to the proxy"); + assertEquals("http://origin.example.com/resource?a=b", requestTarget); + } + + @Test + public void proxiedRequestTargetWithoutUserInfoIsUnchanged() { + Request request = get("http://origin.example.com/resource?a=b").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest proxied = factory().newNettyRequest(request, false, proxy, null, null); + + assertEquals("http://origin.example.com/resource?a=b", proxied.getHttpRequest().uri()); + } + + @Test + public void directRequestTargetStaysRelative() { + Request request = get("http://user:secret@origin.example.com/resource?a=b").build(); + + NettyRequest direct = factory().newNettyRequest(request, false, null, null, null); + + assertEquals("/resource?a=b", direct.getHttpRequest().uri()); + } +}