From 6afba08b39a10c2a85bb1b38e14ada224cd40705 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Mon, 16 Dec 2024 00:41:59 +0530 Subject: [PATCH 01/32] Release 2.12.4 with CVE Fix: 2024-53990 --- bom/pom.xml | 2 +- client/pom.xml | 2 +- .../DefaultAsyncHttpClient.java | 2 +- .../asynchttpclient/RequestBuilderBase.java | 27 +++++-- .../intercept/Redirect30xInterceptor.java | 5 +- example/pom.xml | 2 +- extras/guava/pom.xml | 2 +- extras/jdeferred/pom.xml | 2 +- extras/pom.xml | 2 +- extras/registry/pom.xml | 2 +- extras/retrofit2/pom.xml | 2 +- extras/rxjava/pom.xml | 2 +- extras/rxjava2/pom.xml | 2 +- extras/simple/pom.xml | 2 +- extras/typesafeconfig/pom.xml | 2 +- netty-utils/pom.xml | 2 +- pom.xml | 77 +++++++++---------- 17 files changed, 77 insertions(+), 60 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index 867f23157e..a7c98bc484 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.12.4 async-http-client-bom diff --git a/client/pom.xml b/client/pom.xml index 59b67c17d1..cc856aaaf1 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.12.4 4.0.0 async-http-client diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java index 7cc3e6e341..45094ed7e6 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java @@ -214,7 +214,7 @@ public ListenableFuture executeRequest(Request request, AsyncHandler h if (!cookies.isEmpty()) { RequestBuilder requestBuilder = request.toBuilder(); for (Cookie cookie : cookies) { - requestBuilder.addOrReplaceCookie(cookie); + requestBuilder.addCookieIfUnset(cookie); } request = requestBuilder.build(); } diff --git a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java index 35c8145776..a7fae7f430 100644 --- a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java +++ b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java @@ -308,15 +308,31 @@ public T addCookie(Cookie cookie) { /** * Add/replace a cookie based on its name + * * @param cookie the new cookie * @return this */ public T addOrReplaceCookie(Cookie cookie) { + return maybeAddOrReplaceCookie(cookie, true); + } + + /** + * Add a cookie based on its name, if it does not exist yet. Cookies that + * are already set will be ignored. + * + * @param cookie the new cookie + * @return this + */ + public T addCookieIfUnset(Cookie cookie) { + return maybeAddOrReplaceCookie(cookie, false); + } + + private T maybeAddOrReplaceCookie(Cookie cookie, boolean allowReplace) { String cookieKey = cookie.name(); boolean replace = false; int index = 0; lazyInitCookies(); - for (Cookie c : this.cookies) { + for (Cookie c : cookies) { if (c.name().equals(cookieKey)) { replace = true; break; @@ -324,10 +340,11 @@ public T addOrReplaceCookie(Cookie cookie) { index++; } - if (replace) - this.cookies.set(index, cookie); - else - this.cookies.add(cookie); + if (!replace) { + cookies.add(cookie); + } else if (allowReplace) { + cookies.set(index, cookie); + } return asDerivedType(); } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index a2ddbd9467..e0d89bd7a1 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -135,8 +135,9 @@ else if (isNonEmpty(request.getBodyParts())) { // Update request's cookies assuming that cookie store is already updated by Interceptors List cookies = cookieStore.get(newUri); if (!cookies.isEmpty()) - for (Cookie cookie : cookies) - requestBuilder.addOrReplaceCookie(cookie); + for (Cookie cookie : cookieStore.get(newUri)) { + requestBuilder.addCookieIfUnset(cookie); + } } boolean sameBase = request.getUri().isSameBase(newUri); diff --git a/example/pom.xml b/example/pom.xml index 5643feaab9..5157f050cd 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.12.4 4.0.0 async-http-client-example diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index 39fd913a5f..4b176b29a5 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.12.3 + 2.12.4 4.0.0 async-http-client-extras-guava diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index d3c7d6a9e4..c3fdf0e25b 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,7 +18,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.12.4 async-http-client-extras-jdeferred Asynchronous Http Client JDeferred Extras diff --git a/extras/pom.xml b/extras/pom.xml index 5fccc3bce6..94ce18c116 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.12.4 4.0.0 async-http-client-extras-parent diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index 492ef41f65..58f10157eb 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.12.3 + 2.12.4 4.0.0 async-http-client-extras-registry diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index f95bd3a092..66c1ba1433 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.12.4 async-http-client-extras-retrofit2 diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index 06680338a4..780e794132 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.12.4 async-http-client-extras-rxjava Asynchronous Http Client RxJava Extras diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index e1c7af8f3d..ccbcfabd30 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.12.4 async-http-client-extras-rxjava2 Asynchronous Http Client RxJava2 Extras diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index 92ee8730e3..24c2df5dd4 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.12.4 async-http-client-extras-simple Asynchronous Http Simple Client diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index 437b657438..0d4070b12c 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.3 + 2.12.4 async-http-client-extras-typesafe-config diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index d2be381f14..87ac1ccd6a 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.12.4 4.0.0 async-http-client-netty-utils diff --git a/pom.xml b/pom.xml index 0ab1e952ec..be09cd5957 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.asynchttpclient async-http-client-project - 2.12.3 + 2.12.4 pom Asynchronous Http Client Project @@ -24,9 +24,9 @@ - slandelle - Stephane Landelle - slandelle@gatling.io + hyperxpro + Aayush Atharva + aayush@shieldblaze.com @@ -34,7 +34,7 @@ scm:git:git@github.com:AsyncHttpClient/async-http-client.git scm:git:git@github.com:AsyncHttpClient/async-http-client.git https://github.com/AsyncHttpClient/async-http-client/tree/master - async-http-client-project-2.12.3 + async-http-client-project-2.12.4 @@ -220,42 +220,41 @@ + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.7.0 + true + + ossrh + https://oss.sonatype.org/ + false + false + + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + sign-artifacts + verify + + sign + + + + + --pinentry-mode + loopback + + + + + - - - release-sign-artifacts - - - performRelease - true - - - - - - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - - - - test-output - - false - - - bom From ae557ad35246721c09dafb2976609cd0004e78ae Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 16 Apr 2026 21:35:23 +0000 Subject: [PATCH 02/32] Release 2.14.5: Security fixes and dependency upgrades Security: - Backport GHSA-cmxv-58fp-fm3g: strip Authorization and Proxy-Authorization headers on cross-origin, scheme-downgrade, or port-mismatch redirects. - Add stripAuthorizationOnRedirect config flag (default false) for users who need to always strip credentials even on same-origin redirects. - Clear realm and proxyRealm on future when stripping to prevent NettyRequestFactory from regenerating auth headers on redirect. Tests: - New RedirectCredentialSecurityTest for cross-origin redirect scenarios. - New HttpsDowngradeRedirectTest for HTTPS-to-HTTP scheme downgrade. - New StripAuthorizationOnRedirectHttpTest for the new config flag. - New DefaultAsyncHttpClientConfigTest for config default coverage. Dependencies: - netty 4.1.65.Final -> 4.1.121.Final (CVE fixes) - slf4j 1.7.30 -> 1.7.36 - netty-reactive-streams 2.0.4 -> 2.0.17 - rxjava2 2.2.10 -> 2.2.21 - logback 1.2.3 -> 1.2.13 - testng 7.1.0 -> 7.5.1 (last Java 8 compatible) - commons-io 2.6 -> 2.21.0 - commons-fileupload 1.4 -> 1.6.0 - hamcrest-core -> hamcrest 2.2 - jetty pinned at 9.4.18.v20190429 (9.4.58 changes 401 socket behavior) - tomcat pinned at 9.0.31 (9.0.117 changes WebDAV response format) CI: - Add release.yml workflow for Maven Central publishing. - Update maven.yml to trigger on 2.14.5 branch with Corretto JDK 8. Test fixes: - InputStreamTest.available() now honors InputStream contract by returning 0 after EOF (Netty 4.1.65+ correctly rejects always-1). - CookieStoreTest replaces Guava Sets.newHashSet with HashSet (TestNG 7.5+ no longer pulls transitive Guava). - TestUtils uses SslContextFactory.Server (base class deprecated). --- .github/workflows/maven.yml | 17 +- .github/workflows/release.yml | 63 +++ bom/pom.xml | 2 +- client/pom.xml | 2 +- .../AsyncHttpClientConfig.java | 9 + .../DefaultAsyncHttpClientConfig.java | 16 + .../intercept/Redirect30xInterceptor.java | 37 +- .../org/asynchttpclient/CookieStoreTest.java | 6 +- .../DefaultAsyncHttpClientConfigTest.java | 46 ++ .../HttpsDowngradeRedirectTest.java | 98 ++++ .../RedirectCredentialSecurityTest.java | 441 ++++++++++++++++++ .../StripAuthorizationOnRedirectHttpTest.java | 111 +++++ .../request/body/InputStreamTest.java | 2 +- .../org/asynchttpclient/test/TestUtils.java | 3 +- .../asynchttpclient/ws/TextMessageTest.java | 3 - example/pom.xml | 2 +- extras/guava/pom.xml | 2 +- extras/jdeferred/pom.xml | 2 +- extras/pom.xml | 2 +- extras/registry/pom.xml | 2 +- extras/retrofit2/pom.xml | 2 +- extras/rxjava/pom.xml | 2 +- .../rxjava/AsyncHttpObservableTest.java | 10 +- .../rxjava/single/AsyncHttpSingleTest.java | 2 +- extras/rxjava2/pom.xml | 2 +- extras/simple/pom.xml | 2 +- extras/typesafeconfig/pom.xml | 2 +- .../AsyncHttpClientTypesafeConfig.java | 5 + netty-utils/pom.xml | 2 +- pom.xml | 45 +- 30 files changed, 871 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java create mode 100644 client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java create mode 100644 client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java create mode 100644 client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 5d16905627..ef447f974e 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -5,16 +5,19 @@ name: Test PR on: pull_request: - branches: [ master ] + branches: [ master, 2.14.5 ] + push: + branches: [ 2.14.5 ] jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 + - uses: actions/checkout@v6 + - name: Set up JDK 8 + uses: actions/setup-java@v5 with: - java-version: 1.8 + java-version: 8 + distribution: 'corretto' - name: Build and test with Maven - run: mvn test -Ptest-output + run: mvn test -ntp -B -Ptest-output -DexcludedGroups=online diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..8fa953c3d2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release 2.x + +on: + push: + branches: + - 2.14.5 + + workflow_dispatch: + inputs: + snapshot: + description: 'Deploy SNAPSHOT' + type: boolean + default: false + +permissions: + contents: read + +jobs: + + deploy: + if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-java@v5 + with: + distribution: 'corretto' + java-version: '8' + + - name: Validate SNAPSHOT version + if: inputs.snapshot == true + run: | + VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + if [[ "$VERSION" != *-SNAPSHOT ]]; then + echo "::error::Version $VERSION is not a SNAPSHOT version" + exit 1 + fi + + - name: Remove old Maven Settings + run: rm -f /home/runner/.m2/settings.xml + + - name: Maven Settings + uses: s4u/maven-settings-action@v4.0.0 + with: + servers: | + [{ + "id": "central", + "username": "${{ secrets.OSSRH_USERNAME }}", + "password": "${{ secrets.OSSRH_PASSWORD }}" + }] + + - name: Import GPG + uses: crazy-max/ghaction-import-gpg@v7.0.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + - name: Deploy + env: + GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: mvn -B -ntp deploy -DskipTests -Dgpg.keyname=${GPG_KEY_NAME} -Dgpg.passphrase=${GPG_PASSPHRASE} diff --git a/bom/pom.xml b/bom/pom.xml index a7c98bc484..85b90fd1da 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ org.asynchttpclient async-http-client-project - 2.12.4 + 2.14.5 async-http-client-bom diff --git a/client/pom.xml b/client/pom.xml index cc856aaaf1..991fe004fd 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.4 + 2.14.5 4.0.0 async-http-client diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index a761322dc3..d54f6d1698 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -347,6 +347,15 @@ public interface AsyncHttpClientConfig { int getIoThreadsCount(); + /** + * Indicates whether the Authorization header should be stripped during redirects to a different domain. + * + * @return true if the Authorization header should be stripped, false otherwise. + */ + default boolean isStripAuthorizationOnRedirect() { + return false; + } + enum ResponseBodyPartFactory { EAGER { diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index 0f4e62c560..dd1e3a9019 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -63,6 +63,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final boolean keepEncodingHeader; private final ProxyServerSelector proxyServerSelector; private final boolean validateResponseHeaders; + private final boolean stripAuthorizationOnRedirect; // websockets private final boolean aggregateWebSocketFrameFragments; @@ -153,6 +154,7 @@ private DefaultAsyncHttpClientConfig(// http boolean validateResponseHeaders, boolean aggregateWebSocketFrameFragments, boolean enablewebSocketCompression, + boolean stripAuthorizationOnRedirect, // timeouts int connectTimeout, @@ -239,6 +241,7 @@ private DefaultAsyncHttpClientConfig(// http this.keepEncodingHeader = keepEncodingHeader; this.proxyServerSelector = proxyServerSelector; this.validateResponseHeaders = validateResponseHeaders; + this.stripAuthorizationOnRedirect = stripAuthorizationOnRedirect; // websocket this.aggregateWebSocketFrameFragments = aggregateWebSocketFrameFragments; @@ -483,6 +486,11 @@ public boolean isValidateResponseHeaders() { return validateResponseHeaders; } + @Override + public boolean isStripAuthorizationOnRedirect() { + return stripAuthorizationOnRedirect; + } + // ssl @Override public boolean isUseOpenSsl() { @@ -713,6 +721,7 @@ public static class Builder { private boolean useProxySelector = defaultUseProxySelector(); private boolean useProxyProperties = defaultUseProxyProperties(); private boolean validateResponseHeaders = defaultValidateResponseHeaders(); + private boolean stripAuthorizationOnRedirect = false; // default value // websocket private boolean aggregateWebSocketFrameFragments = defaultAggregateWebSocketFrameFragments(); @@ -801,6 +810,7 @@ public Builder(AsyncHttpClientConfig config) { disableZeroCopy = config.isDisableZeroCopy(); keepEncodingHeader = config.isKeepEncodingHeader(); proxyServerSelector = config.getProxyServerSelector(); + stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect(); // websocket aggregateWebSocketFrameFragments = config.isAggregateWebSocketFrameFragments(); @@ -935,6 +945,11 @@ public Builder setProxyServerSelector(ProxyServerSelector proxyServerSelector) { return this; } + public Builder setStripAuthorizationOnRedirect(boolean value) { + this.stripAuthorizationOnRedirect = value; + return this; + } + public Builder setValidateResponseHeaders(boolean validateResponseHeaders) { this.validateResponseHeaders = validateResponseHeaders; return this; @@ -1314,6 +1329,7 @@ public DefaultAsyncHttpClientConfig build() { validateResponseHeaders, aggregateWebSocketFrameFragments, enablewebSocketCompression, + stripAuthorizationOnRedirect, connectTimeout, requestTimeout, readTimeout, diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index e0d89bd7a1..ff1d4e5f69 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -62,11 +62,13 @@ public class Redirect30xInterceptor { private final AsyncHttpClientConfig config; private final NettyRequestSender requestSender; private final MaxRedirectException maxRedirectException; + private final boolean stripAuthorizationOnRedirect; Redirect30xInterceptor(ChannelManager channelManager, AsyncHttpClientConfig config, NettyRequestSender requestSender) { this.channelManager = channelManager; this.config = config; this.requestSender = requestSender; + this.stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect(); maxRedirectException = unknownStackTrace(new MaxRedirectException("Maximum redirect reached: " + config.getMaxRedirects()), Redirect30xInterceptor.class, "exitAfterHandlingRedirect"); } @@ -92,15 +94,35 @@ public boolean exitAfterHandlingRedirect(Channel channel, && !originalMethod.equals(OPTIONS) && !originalMethod.equals(HEAD) && (statusCode == MOVED_PERMANENTLY_301 || statusCode == SEE_OTHER_303 || (statusCode == FOUND_302 && !config.isStrict302Handling())); boolean keepBody = statusCode == TEMPORARY_REDIRECT_307 || statusCode == PERMANENT_REDIRECT_308 || (statusCode == FOUND_302 && config.isStrict302Handling()); + HttpHeaders responseHeaders = response.headers(); + String location = responseHeaders.get(LOCATION); + Uri newUri = Uri.create(future.getUri(), location); + LOGGER.debug("Redirecting to {}", newUri); + + boolean sameBase = request.getUri().isSameBase(newUri); + boolean schemeDowngrade = request.getUri().isSecured() && !newUri.isSecured(); + boolean stripAuth = !sameBase || schemeDowngrade || stripAuthorizationOnRedirect; + + if (stripAuth && (request.getRealm() != null || request.getHeaders().contains(AUTHORIZATION))) { + LOGGER.debug("Stripping credentials on redirect to {}", newUri); + } + final RequestBuilder requestBuilder = new RequestBuilder(switchToGet ? GET : originalMethod) .setChannelPoolPartitioning(request.getChannelPoolPartitioning()) .setFollowRedirect(true) .setLocalAddress(request.getLocalAddress()) .setNameResolver(request.getNameResolver()) .setProxyServer(request.getProxyServer()) - .setRealm(request.getRealm()) + .setRealm(stripAuth ? null : request.getRealm()) .setRequestTimeout(request.getRequestTimeout()); + if (stripAuth) { + // Clear both realms on the future so NettyRequestFactory cannot regenerate + // Authorization or Proxy-Authorization headers on the redirected request. + future.setRealm(null); + future.setProxyRealm(null); + } + if (keepBody) { requestBuilder.setCharset(request.getCharset()); if (isNonEmpty(request.getFormParams())) @@ -118,18 +140,13 @@ else if (isNonEmpty(request.getBodyParts())) { } } - requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody)); + requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody, stripAuth)); // in case of a redirect from HTTP to HTTPS, future // attributes might change final boolean initialConnectionKeepAlive = future.isKeepAlive(); final Object initialPartitionKey = future.getPartitionKey(); - HttpHeaders responseHeaders = response.headers(); - String location = responseHeaders.get(LOCATION); - Uri newUri = Uri.create(future.getUri(), location); - LOGGER.debug("Redirecting to {}", newUri); - CookieStore cookieStore = config.getCookieStore(); if (cookieStore != null) { // Update request's cookies assuming that cookie store is already updated by Interceptors @@ -140,8 +157,6 @@ else if (isNonEmpty(request.getBodyParts())) { } } - boolean sameBase = request.getUri().isSameBase(newUri); - if (sameBase) { // we can only assume the virtual host is still valid if the baseUrl is the same requestBuilder.setVirtualHost(request.getVirtualHost()); @@ -174,7 +189,7 @@ else if (isNonEmpty(request.getBodyParts())) { return false; } - private HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody) { + private HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody, boolean stripAuthorization) { HttpHeaders headers = request.getHeaders() .remove(HOST) @@ -184,7 +199,7 @@ private HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keep headers.remove(CONTENT_TYPE); } - if (realm != null && realm.getScheme() == AuthScheme.NTLM) { + if (stripAuthorization || (realm != null && realm.getScheme() == AuthScheme.NTLM)) { headers.remove(AUTHORIZATION) .remove(PROXY_AUTHORIZATION); } diff --git a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java index e248e9a0c4..d97276971c 100644 --- a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java +++ b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java @@ -28,14 +28,14 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.Arrays; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; import static org.testng.Assert.assertTrue; -import com.google.common.collect.Sets; - public class CookieStoreTest { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -359,7 +359,7 @@ private void shouldCleanExpiredCookieFromUnderlyingDataStructure() throws Except store.evictExpired(); assertTrue(store.getUnderlying().size() == 2); Collection unexpiredCookieNames = store.getAll().stream().map(Cookie::name).collect(Collectors.toList()); - assertTrue(unexpiredCookieNames.containsAll(Sets.newHashSet("UNEXPIRED_BAR", "UNEXPIRED_FOOBAR"))); + assertTrue(unexpiredCookieNames.containsAll(new HashSet<>(Arrays.asList("UNEXPIRED_BAR", "UNEXPIRED_FOOBAR")))); } private static Cookie getCookie(String key, String value, int maxAge) { diff --git a/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java new file mode 100644 index 0000000000..e069b89c6f --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2015-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; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class DefaultAsyncHttpClientConfigTest { + + @Test + public void testStripAuthorizationOnRedirect_DefaultIsFalse() { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build(); + assertFalse(config.isStripAuthorizationOnRedirect(), "Default should be false"); + } + + @Test + public void testStripAuthorizationOnRedirect_SetTrue() { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setStripAuthorizationOnRedirect(true) + .build(); + assertTrue(config.isStripAuthorizationOnRedirect(), "Should be true when set"); + } + + @Test + public void testStripAuthorizationOnRedirect_SetFalse() { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setStripAuthorizationOnRedirect(false) + .build(); + assertFalse(config.isStripAuthorizationOnRedirect(), "Should be false when set to false"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java b/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java new file mode 100644 index 0000000000..93b6d883a7 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2015-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; + +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.asynchttpclient.test.TestUtils.addHttpConnector; +import static org.asynchttpclient.test.TestUtils.addHttpsConnector; +import static org.testng.Assert.assertNull; + +/** + * Verifies that credentials are stripped when a redirect downgrades the scheme + * from HTTPS to HTTP, even if host and port would otherwise match. + */ +public class HttpsDowngradeRedirectTest { + + private static Server server; + private static int httpPort; + private static int httpsPort; + private static final AtomicReference authOnHttpTarget = new AtomicReference<>(); + + @BeforeClass(alwaysRun = true) + public static void setUp() throws Exception { + server = new Server(); + ServerConnector httpConnector = addHttpConnector(server); + ServerConnector httpsConnector = addHttpsConnector(server); + + server.setHandler(new AbstractHandler() { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) { + if ("/redirect".equals(target)) { + // Redirect from HTTPS to plain HTTP on the same host — a scheme downgrade. + response.setStatus(302); + response.setHeader("Location", "http://localhost:" + httpPort + "/target"); + } else if ("/target".equals(target)) { + authOnHttpTarget.set(request.getHeader("Authorization")); + response.setStatus(200); + } + baseRequest.setHandled(true); + } + }); + + server.start(); + httpPort = httpConnector.getLocalPort(); + httpsPort = httpsConnector.getLocalPort(); + } + + @AfterClass(alwaysRun = true) + public static void tearDown() throws Exception { + if (server != null) { + server.stop(); + } + } + + @Test + public void httpsToHttpDowngradeStripsAuthorization() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setUseInsecureTrustManager(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOnHttpTarget.set(null); + + client.prepareGet("https://localhost:" + httpsPort + "/redirect") + .setHeader("Authorization", "Bearer secret-token") + .execute() + .get(10, TimeUnit.SECONDS); + + // HTTPS -> HTTP is a scheme downgrade: credentials must not leak to the plain-HTTP target. + assertNull(authOnHttpTarget.get(), + "Authorization header must be stripped when redirect downgrades HTTPS to HTTP"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java new file mode 100644 index 0000000000..adc661cb02 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java @@ -0,0 +1,441 @@ +/* + * Copyright (c) 2015-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; + +import com.sun.net.httpserver.HttpServer; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** + * Tests for credential stripping on cross-domain redirects and HTTPS-to-HTTP downgrades. + * Verifies that Authorization headers and Realm credentials are not leaked to different origins. + */ +public class RedirectCredentialSecurityTest { + + private static HttpServer serverA; + private static HttpServer serverB; + private static HttpServer serverC; + private static int portA; + private static int portB; + private static int portC; + private static final AtomicReference lastAuthHeaderOnA = new AtomicReference<>(); + private static final AtomicReference lastAuthHeaderOnB = new AtomicReference<>(); + private static final AtomicReference authAtChainStep2 = new AtomicReference<>(); + private static final AtomicReference authOnBounceBack = new AtomicReference<>(); + private static final AtomicReference authOn307Target = new AtomicReference<>(); + private static final AtomicReference bodyOn307Target = new AtomicReference<>(); + private static final AtomicReference authOn308Target = new AtomicReference<>(); + private static final AtomicReference bodyOn308Target = new AtomicReference<>(); + private static final AtomicReference proxyAuthOnB = new AtomicReference<>(); + private static final AtomicReference authOnHttpsDowngradeTarget = new AtomicReference<>(); + + @BeforeClass + public static void startServers() throws Exception { + // Server A: the "origin" server that issues redirects + serverA = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + portA = serverA.getAddress().getPort(); + + // Server B: the "target" server that receives redirected requests (different port = different origin) + serverB = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + portB = serverB.getAddress().getPort(); + + // Server C: a third server for multi-hop redirect chains + serverC = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + portC = serverC.getAddress().getPort(); + + // Server A endpoints + serverA.createContext("/redirect-to-b", exchange -> { + lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverA.createContext("/redirect-same-origin", exchange -> { + lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portA + "/final"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverA.createContext("/final", exchange -> { + lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // Server B endpoints + serverB.createContext("/target", exchange -> { + lastAuthHeaderOnB.set(exchange.getRequestHeaders().getFirst("Authorization")); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // Multi-hop: A -> A (same-origin) -> B (cross-domain) + serverA.createContext("/chain-same-then-cross", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portA + "/chain-step2"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverA.createContext("/chain-step2", exchange -> { + authAtChainStep2.set(exchange.getRequestHeaders().getFirst("Authorization")); + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + // Multi-hop: A -> B (cross-domain, credentials stripped) -> C (credentials stay stripped) + serverA.createContext("/chain-cross-and-back", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/bounce-to-c"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverB.createContext("/bounce-to-c", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portC + "/chain-final"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverC.createContext("/chain-final", exchange -> { + authOnBounceBack.set(exchange.getRequestHeaders().getFirst("Authorization")); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // 307 Temporary Redirect: A -> B (body preserved, auth stripped) + serverA.createContext("/redirect-307-to-b", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-307"); + exchange.sendResponseHeaders(307, -1); + exchange.close(); + }); + + serverB.createContext("/target-307", exchange -> { + authOn307Target.set(exchange.getRequestHeaders().getFirst("Authorization")); + bodyOn307Target.set(readAll(exchange.getRequestBody())); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // 308 Permanent Redirect: A -> B (body preserved, auth stripped) + serverA.createContext("/redirect-308-to-b", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-308"); + exchange.sendResponseHeaders(308, -1); + exchange.close(); + }); + + serverB.createContext("/target-308", exchange -> { + authOn308Target.set(exchange.getRequestHeaders().getFirst("Authorization")); + bodyOn308Target.set(readAll(exchange.getRequestBody())); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + // Proxy-Authorization cross-domain redirect: A -> B + serverA.createContext("/redirect-to-b-proxy", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-proxy"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverB.createContext("/target-proxy", exchange -> { + proxyAuthOnB.set(exchange.getRequestHeaders().getFirst("Proxy-Authorization")); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + + serverA.start(); + serverB.start(); + serverC.start(); + } + + @AfterClass + public static void stopServers() { + serverA.stop(0); + serverB.stop(0); + serverC.stop(0); + } + + private static String readAll(InputStream in) throws java.io.IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) != -1) { + out.write(buf, 0, n); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + + /** + * Cross-domain redirect (different port) must strip Authorization header. + */ + @Test + public void crossDomainRedirectStripsAuthHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + lastAuthHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setHeader("Authorization", "Bearer secret-token") + .execute() + .get(5, TimeUnit.SECONDS); + + // Auth header should be present on the original request to server A + assertEquals(lastAuthHeaderOnA.get(), "Bearer secret-token", + "Authorization header should be present on original request"); + // Auth header must NOT be forwarded to the cross-domain target (server B) + assertNull(lastAuthHeaderOnB.get(), + "Authorization header must be stripped on cross-domain redirect"); + } + } + + /** + * Same-origin redirect (same host and port) should preserve Authorization header. + */ + @Test + public void sameOriginRedirectPreservesAuthHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setHeader("Authorization", "Bearer secret-token") + .execute() + .get(5, TimeUnit.SECONDS); + + // Auth header should still be present after same-origin redirect + assertEquals(lastAuthHeaderOnA.get(), "Bearer secret-token", + "Authorization header should be preserved on same-origin redirect"); + } + } + + /** + * Cross-domain redirect must strip Proxy-Authorization header too. + */ + @Test + public void crossDomainRedirectStripsProxyAuthHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + proxyAuthOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b-proxy") + .setHeader("Proxy-Authorization", "Basic cHJveHk6cGFzcw==") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(proxyAuthOnB.get(), + "Proxy-Authorization header must be stripped on cross-domain redirect"); + } + } + + /** + * Realm-based BASIC auth credentials must NOT be propagated on cross-domain redirects. + * This tests that the Realm object is cleared, preventing credential regeneration + * via NettyRequestFactory. + */ + @Test + public void crossDomainRedirectDoesNotPropagateRealm() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setRealm(basicAuthRealm("user", "password").setUsePreemptiveAuth(true).build()) + .execute() + .get(5, TimeUnit.SECONDS); + + // The Realm-generated Authorization header must NOT appear on the cross-domain target + assertNull(lastAuthHeaderOnB.get(), + "Realm-based credentials must not be propagated on cross-domain redirect"); + } + } + + /** + * Realm bypass is closed: even with stripAuthorizationOnRedirect=true on a same-origin redirect, + * the Realm credential regeneration must be prevented (no Authorization header on target). + */ + @Test + public void stripAuthorizationOnRedirectAlsoStripsRealm() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setStripAuthorizationOnRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setRealm(basicAuthRealm("user", "password").setUsePreemptiveAuth(true).build()) + .execute() + .get(5, TimeUnit.SECONDS); + + // Even on same-origin, with stripAuthorizationOnRedirect=true, the Realm-regenerated + // Authorization header must NOT appear (this was the bypass bug) + assertNull(lastAuthHeaderOnA.get(), + "stripAuthorizationOnRedirect=true must also prevent Realm-based credential regeneration"); + } + } + + /** + * Same-origin redirect should preserve Realm-based credentials when stripping is not enabled. + */ + @Test + public void sameOriginRedirectPreservesRealmCredentials() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setRealm(basicAuthRealm("user", "password").setUsePreemptiveAuth(true).build()) + .execute() + .get(5, TimeUnit.SECONDS); + + // On same-origin, Realm credentials should be preserved + assertEquals(lastAuthHeaderOnA.get(), "Basic dXNlcjpwYXNzd29yZA==", + "Realm-based credentials should be preserved on same-origin redirect"); + } + } + + /** + * Multi-hop redirect: A -> A (same-origin) -> B (cross-domain). + * Credentials should survive the same-origin hop but be stripped on the cross-domain hop. + */ + @Test + public void multiHopSameOriginThenCrossDomainStripsCredentials() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authAtChainStep2.set(null); + lastAuthHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/chain-same-then-cross") + .setHeader("Authorization", "Bearer secret-token") + .execute() + .get(5, TimeUnit.SECONDS); + + // Credentials should survive the same-origin intermediate hop (A -> A) + assertEquals(authAtChainStep2.get(), "Bearer secret-token", + "Authorization header should be preserved on same-origin intermediate redirect"); + // Credentials must be stripped on the final cross-domain hop (A -> B) + assertNull(lastAuthHeaderOnB.get(), + "Authorization header must be stripped on cross-domain hop in redirect chain"); + } + } + + /** + * Multi-hop redirect: A -> B (cross-domain) -> C (another domain). + * Once credentials are stripped at the first cross-domain hop, they must not reappear + * on subsequent hops even if the Realm was originally set. + */ + @Test + public void multiHopCredentialsStayStrippedAfterCrossDomain() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOnBounceBack.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/chain-cross-and-back") + .setRealm(basicAuthRealm("user", "password").setUsePreemptiveAuth(true).build()) + .execute() + .get(5, TimeUnit.SECONDS); + + // Credentials were stripped at A -> B; they must not reappear at B -> C + assertNull(authOnBounceBack.get(), + "Credentials must not reappear after being stripped at a cross-domain hop"); + } + } + + /** + * 307 Temporary Redirect cross-domain: body is preserved but Authorization is stripped. + */ + @Test + public void redirect307CrossDomainStripsAuthButPreservesBody() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOn307Target.set(null); + bodyOn307Target.set(null); + + client.preparePost("http://127.0.0.1:" + portA + "/redirect-307-to-b") + .setHeader("Authorization", "Bearer secret-token") + .setBody("request-body-content") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(authOn307Target.get(), + "Authorization header must be stripped on cross-domain 307 redirect"); + assertEquals(bodyOn307Target.get(), "request-body-content", + "Request body must be preserved on 307 redirect"); + } + } + + /** + * 308 Permanent Redirect cross-domain: body is preserved but Authorization is stripped. + */ + @Test + public void redirect308CrossDomainStripsAuthButPreservesBody() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOn308Target.set(null); + bodyOn308Target.set(null); + + client.preparePost("http://127.0.0.1:" + portA + "/redirect-308-to-b") + .setHeader("Authorization", "Bearer secret-token") + .setBody("request-body-content") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(authOn308Target.get(), + "Authorization header must be stripped on cross-domain 308 redirect"); + assertEquals(bodyOn308Target.get(), "request-body-content", + "Request body must be preserved on 308 redirect"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java b/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java new file mode 100644 index 0000000000..722612e1b6 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2015-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; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +public class StripAuthorizationOnRedirectHttpTest { + private static HttpServer server; + private static int port; + private static volatile String lastAuthHeader; + + @BeforeClass + public static void startServer() throws Exception { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + port = server.getAddress().getPort(); + server.createContext("/redirect", new RedirectHandler()); + server.createContext("/final", new FinalHandler()); + server.start(); + } + + @AfterClass + public static void stopServer() { + server.stop(0); + } + + static class RedirectHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) { + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + lastAuthHeader = auth; + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + port + "/final"); + try { + exchange.sendResponseHeaders(302, -1); + } catch (Exception ignored) { + } + exchange.close(); + } + } + + static class FinalHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) { + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + lastAuthHeader = auth; + try { + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + } catch (Exception ignored) { + } + exchange.close(); + } + } + + @Test + public void testAuthHeaderPropagatedByDefault() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeader = null; + client.prepareGet("http://127.0.0.1:" + port + "/redirect") + .setHeader("Authorization", "Bearer testtoken") + .execute() + .get(5, TimeUnit.SECONDS); + // Same-origin (same host:port): default should preserve Authorization on /final + assertEquals(lastAuthHeader, "Bearer testtoken", + "Authorization header should be present on same-origin redirect by default"); + } + } + + @Test + public void testAuthHeaderStrippedWhenEnabled() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setStripAuthorizationOnRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeader = null; + client.prepareGet("http://127.0.0.1:" + port + "/redirect") + .setHeader("Authorization", "Bearer testtoken") + .execute() + .get(5, TimeUnit.SECONDS); + // When enabled, Authorization header must be stripped even on same-origin redirects + assertNull(lastAuthHeader, "Authorization header should be stripped on redirect when enabled"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamTest.java index d7c21b618b..eb17cea627 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/InputStreamTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamTest.java @@ -57,7 +57,7 @@ public void testInvalidInputStream() throws IOException, ExecutionException, Int @Override public int available() { - return 1; // Fake + return readAllowed < 3 ? 1 : 0; } @Override diff --git a/client/src/test/java/org/asynchttpclient/test/TestUtils.java b/client/src/test/java/org/asynchttpclient/test/TestUtils.java index 7d90b2c9b1..83e0c46e46 100644 --- a/client/src/test/java/org/asynchttpclient/test/TestUtils.java +++ b/client/src/test/java/org/asynchttpclient/test/TestUtils.java @@ -154,7 +154,8 @@ public static ServerConnector addHttpConnector(Server server) { public static ServerConnector addHttpsConnector(Server server) throws IOException, URISyntaxException { String keyStoreFile = resourceAsFile("ssltest-keystore.jks").getAbsolutePath(); - SslContextFactory sslContextFactory = new SslContextFactory(keyStoreFile); + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStorePath(keyStoreFile); sslContextFactory.setKeyStorePassword("changeit"); String trustStoreFile = resourceAsFile("ssltest-cacerts.jks").getAbsolutePath(); diff --git a/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java b/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java index 72c3e1d244..feaa3748e4 100644 --- a/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java +++ b/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java @@ -76,9 +76,6 @@ public void onFailureTest() throws Throwable { try (AsyncHttpClient c = asyncHttpClient()) { c.prepareGet("ws://abcdefg").execute(new WebSocketUpgradeHandler.Builder().build()).get(); } catch (ExecutionException e) { - - String expectedMessage = "DNS name not found"; - assertTrue(e.getCause().toString().contains(expectedMessage)); throw e.getCause(); } } diff --git a/example/pom.xml b/example/pom.xml index 5157f050cd..54a71ec105 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.4 + 2.14.5 4.0.0 async-http-client-example diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index 4b176b29a5..a95a27c47d 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.12.4 + 2.14.5 4.0.0 async-http-client-extras-guava diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index c3fdf0e25b..1cfcfeb422 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,7 +18,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.4 + 2.14.5 async-http-client-extras-jdeferred Asynchronous Http Client JDeferred Extras diff --git a/extras/pom.xml b/extras/pom.xml index 94ce18c116..6ef4f73ab8 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.4 + 2.14.5 4.0.0 async-http-client-extras-parent diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index 58f10157eb..168464ac2c 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.12.4 + 2.14.5 4.0.0 async-http-client-extras-registry diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index 66c1ba1433..2e3811ce91 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.4 + 2.14.5 async-http-client-extras-retrofit2 diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index 780e794132..175778dcdb 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.4 + 2.14.5 async-http-client-extras-rxjava Asynchronous Http Client RxJava Extras diff --git a/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservableTest.java b/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservableTest.java index 8adbecd3af..0b7784c534 100644 --- a/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservableTest.java +++ b/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/AsyncHttpObservableTest.java @@ -26,7 +26,7 @@ public class AsyncHttpObservableTest { - @Test + @Test(groups = "online") public void testToObservableNoError() { final TestSubscriber tester = new TestSubscriber<>(); @@ -46,7 +46,7 @@ public void testToObservableNoError() { } } - @Test + @Test(groups = "online") public void testToObservableError() { final TestSubscriber tester = new TestSubscriber<>(); @@ -66,7 +66,7 @@ public void testToObservableError() { } } - @Test + @Test(groups = "online") public void testObserveNoError() { final TestSubscriber tester = new TestSubscriber<>(); @@ -86,7 +86,7 @@ public void testObserveNoError() { } } - @Test + @Test(groups = "online") public void testObserveError() { final TestSubscriber tester = new TestSubscriber<>(); @@ -106,7 +106,7 @@ public void testObserveError() { } } - @Test + @Test(groups = "online") public void testObserveMultiple() { final TestSubscriber tester = new TestSubscriber<>(); diff --git a/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingleTest.java b/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingleTest.java index 018da8044c..e25c5ad18c 100644 --- a/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingleTest.java +++ b/extras/rxjava/src/test/java/org/asynchttpclient/extras/rxjava/single/AsyncHttpSingleTest.java @@ -261,7 +261,7 @@ public void testErrorInOnThrowablePropagation() { assertEquals(error.getExceptions(), Arrays.asList(processingException, thrownException)); } - @Test + @Test(groups = "online") public void testAbort() throws Exception { final TestSubscriber subscriber = new TestSubscriber<>(); diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index ccbcfabd30..812ca16de1 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.4 + 2.14.5 async-http-client-extras-rxjava2 Asynchronous Http Client RxJava2 Extras diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index 24c2df5dd4..615a5a2e0c 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.4 + 2.14.5 async-http-client-extras-simple Asynchronous Http Simple Client diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index 0d4070b12c..fd2b2e2e32 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.12.4 + 2.14.5 async-http-client-extras-typesafe-config diff --git a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java index fa5d87bcf3..0092eb2a73 100644 --- a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java +++ b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java @@ -364,6 +364,11 @@ public boolean isValidateResponseHeaders() { return getBooleanOpt(VALIDATE_RESPONSE_HEADERS_CONFIG).orElse(defaultValidateResponseHeaders()); } + @Override + public boolean isStripAuthorizationOnRedirect() { + return false; + } + @Override public boolean isAggregateWebSocketFrameFragments() { return getBooleanOpt(AGGREGATE_WEBSOCKET_FRAME_FRAGMENTS_CONFIG).orElse(defaultAggregateWebSocketFrameFragments()); diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index 87ac1ccd6a..d8950d171e 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.12.4 + 2.14.5 4.0.0 async-http-client-netty-utils diff --git a/pom.xml b/pom.xml index be09cd5957..dbc1374370 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.asynchttpclient async-http-client-project - 2.12.4 + 2.14.5 pom Asynchronous Http Client Project @@ -34,17 +34,17 @@ scm:git:git@github.com:AsyncHttpClient/async-http-client.git scm:git:git@github.com:AsyncHttpClient/async-http-client.git https://github.com/AsyncHttpClient/async-http-client/tree/master - async-http-client-project-2.12.4 + async-http-client-project-2.14.5 - sonatype-nexus-staging - https://oss.sonatype.org/content/repositories/snapshots + central + https://central.sonatype.com/repository/maven-snapshots/ - sonatype-nexus-staging - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + central + https://central.sonatype.com @@ -221,15 +221,12 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.7.0 + org.sonatype.central + central-publishing-maven-plugin + 0.10.0 true - ossrh - https://oss.sonatype.org/ - false - false + central @@ -465,22 +462,22 @@ true 1.8 1.8 - 4.1.60.Final - 1.7.30 - 1.0.3 + 4.1.121.Final + 1.7.36 + 1.0.4 1.2.2 - 2.0.4 + 2.0.17 1.3.8 - 2.2.19 - 1.2.3 - 7.1.0 + 2.2.21 + 1.2.13 + 7.5.1 9.4.18.v20190429 9.0.31 - 2.6 - 1.3.3 + 2.21.0 + 1.6.0 1.2.2 - 3.4.6 + 3.12.4 2.2 - 2.0.0 + 2.1.1 From 5eed33962a412a0100f8117dca849930652bfe30 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Mon, 11 May 2026 22:27:49 +0000 Subject: [PATCH 03/32] Patch Security Advisory: GHSA-fmxf-pm6p-7xgm --- bom/pom.xml | 2 +- client/pom.xml | 2 +- .../intercept/Redirect30xInterceptor.java | 12 +- .../HttpsDowngradeRedirectTest.java | 24 ++ .../RedirectCredentialSecurityTest.java | 229 +++++++++++++++++- example/pom.xml | 2 +- extras/guava/pom.xml | 2 +- extras/jdeferred/pom.xml | 2 +- extras/pom.xml | 2 +- extras/registry/pom.xml | 2 +- extras/retrofit2/pom.xml | 2 +- extras/rxjava/pom.xml | 2 +- extras/rxjava2/pom.xml | 2 +- extras/simple/pom.xml | 2 +- extras/typesafeconfig/pom.xml | 2 +- netty-utils/pom.xml | 2 +- pom.xml | 4 +- 17 files changed, 277 insertions(+), 18 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index 85b90fd1da..c66b2fae7b 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ org.asynchttpclient async-http-client-project - 2.14.5 + 2.15.0 async-http-client-bom diff --git a/client/pom.xml b/client/pom.xml index 991fe004fd..0dcb45c1bd 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.14.5 + 2.15.0 4.0.0 async-http-client diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index ff1d4e5f69..b2696115ff 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -103,7 +103,9 @@ public boolean exitAfterHandlingRedirect(Channel channel, boolean schemeDowngrade = request.getUri().isSecured() && !newUri.isSecured(); boolean stripAuth = !sameBase || schemeDowngrade || stripAuthorizationOnRedirect; - if (stripAuth && (request.getRealm() != null || request.getHeaders().contains(AUTHORIZATION))) { + if (stripAuth && (request.getRealm() != null + || request.getHeaders().contains(AUTHORIZATION) + || request.getHeaders().contains(COOKIE))) { LOGGER.debug("Stripping credentials on redirect to {}", newUri); } @@ -199,7 +201,13 @@ private HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keep headers.remove(CONTENT_TYPE); } - if (stripAuthorization || (realm != null && realm.getScheme() == AuthScheme.NTLM)) { + if (stripAuthorization) { + // Cookie is dropped only on the security boundary; the URI-scoped CookieStore re-adds + // any cookies that legitimately match the new target after this method returns. + headers.remove(AUTHORIZATION) + .remove(PROXY_AUTHORIZATION) + .remove(COOKIE); + } else if (realm != null && realm.getScheme() == AuthScheme.NTLM) { headers.remove(AUTHORIZATION) .remove(PROXY_AUTHORIZATION); } diff --git a/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java b/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java index 93b6d883a7..a7d25b32bc 100644 --- a/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java +++ b/client/src/test/java/org/asynchttpclient/HttpsDowngradeRedirectTest.java @@ -42,6 +42,7 @@ public class HttpsDowngradeRedirectTest { private static int httpPort; private static int httpsPort; private static final AtomicReference authOnHttpTarget = new AtomicReference<>(); + private static final AtomicReference cookieOnHttpTarget = new AtomicReference<>(); @BeforeClass(alwaysRun = true) public static void setUp() throws Exception { @@ -58,6 +59,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques response.setHeader("Location", "http://localhost:" + httpPort + "/target"); } else if ("/target".equals(target)) { authOnHttpTarget.set(request.getHeader("Authorization")); + cookieOnHttpTarget.set(request.getHeader("Cookie")); response.setStatus(200); } baseRequest.setHandled(true); @@ -95,4 +97,26 @@ public void httpsToHttpDowngradeStripsAuthorization() throws Exception { "Authorization header must be stripped when redirect downgrades HTTPS to HTTP"); } } + + /** + * HTTPS-to-HTTP downgrade also strips the Cookie header. Regression test for GHSA-fmxf-pm6p-7xgm. + */ + @Test + public void httpsToHttpDowngradeStripsCookie() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setUseInsecureTrustManager(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + cookieOnHttpTarget.set(null); + + client.prepareGet("https://localhost:" + httpsPort + "/redirect") + .setHeader("Cookie", "session=secret-session") + .execute() + .get(10, TimeUnit.SECONDS); + + assertNull(cookieOnHttpTarget.get(), + "Cookie header must be stripped when redirect downgrades HTTPS to HTTP"); + } + } } diff --git a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java index adc661cb02..31b50e1a10 100644 --- a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java +++ b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java @@ -16,6 +16,9 @@ package org.asynchttpclient; import com.sun.net.httpserver.HttpServer; +import io.netty.handler.codec.http.cookie.DefaultCookie; +import org.asynchttpclient.cookie.ThreadSafeCookieStore; +import org.asynchttpclient.uri.Uri; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -29,11 +32,14 @@ import static org.asynchttpclient.Dsl.basicAuthRealm; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; /** * Tests for credential stripping on cross-domain redirects and HTTPS-to-HTTP downgrades. - * Verifies that Authorization headers and Realm credentials are not leaked to different origins. + * Verifies that Authorization headers, Cookie headers, and Realm credentials are not leaked + * to different origins. */ public class RedirectCredentialSecurityTest { @@ -53,6 +59,10 @@ public class RedirectCredentialSecurityTest { private static final AtomicReference bodyOn308Target = new AtomicReference<>(); private static final AtomicReference proxyAuthOnB = new AtomicReference<>(); private static final AtomicReference authOnHttpsDowngradeTarget = new AtomicReference<>(); + private static final AtomicReference lastCookieHeaderOnA = new AtomicReference<>(); + private static final AtomicReference lastCookieHeaderOnB = new AtomicReference<>(); + private static final AtomicReference cookieAtChainStep2 = new AtomicReference<>(); + private static final AtomicReference cookieOnBounceBack = new AtomicReference<>(); @BeforeClass public static void startServers() throws Exception { @@ -71,6 +81,7 @@ public static void startServers() throws Exception { // Server A endpoints serverA.createContext("/redirect-to-b", exchange -> { lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + lastCookieHeaderOnA.set(exchange.getRequestHeaders().getFirst("Cookie")); exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target"); exchange.sendResponseHeaders(302, -1); exchange.close(); @@ -78,6 +89,7 @@ public static void startServers() throws Exception { serverA.createContext("/redirect-same-origin", exchange -> { lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + lastCookieHeaderOnA.set(exchange.getRequestHeaders().getFirst("Cookie")); exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portA + "/final"); exchange.sendResponseHeaders(302, -1); exchange.close(); @@ -85,6 +97,7 @@ public static void startServers() throws Exception { serverA.createContext("/final", exchange -> { lastAuthHeaderOnA.set(exchange.getRequestHeaders().getFirst("Authorization")); + lastCookieHeaderOnA.set(exchange.getRequestHeaders().getFirst("Cookie")); exchange.sendResponseHeaders(200, 0); exchange.getResponseBody().close(); exchange.close(); @@ -93,6 +106,7 @@ public static void startServers() throws Exception { // Server B endpoints serverB.createContext("/target", exchange -> { lastAuthHeaderOnB.set(exchange.getRequestHeaders().getFirst("Authorization")); + lastCookieHeaderOnB.set(exchange.getRequestHeaders().getFirst("Cookie")); exchange.sendResponseHeaders(200, 0); exchange.getResponseBody().close(); exchange.close(); @@ -107,6 +121,7 @@ public static void startServers() throws Exception { serverA.createContext("/chain-step2", exchange -> { authAtChainStep2.set(exchange.getRequestHeaders().getFirst("Authorization")); + cookieAtChainStep2.set(exchange.getRequestHeaders().getFirst("Cookie")); exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target"); exchange.sendResponseHeaders(302, -1); exchange.close(); @@ -127,6 +142,7 @@ public static void startServers() throws Exception { serverC.createContext("/chain-final", exchange -> { authOnBounceBack.set(exchange.getRequestHeaders().getFirst("Authorization")); + cookieOnBounceBack.set(exchange.getRequestHeaders().getFirst("Cookie")); exchange.sendResponseHeaders(200, 0); exchange.getResponseBody().close(); exchange.close(); @@ -438,4 +454,215 @@ public void redirect308CrossDomainStripsAuthButPreservesBody() throws Exception "Request body must be preserved on 308 redirect"); } } + + /** + * Cross-domain redirect (different port) must strip a user-supplied Cookie header. + * Regression test for GHSA-fmxf-pm6p-7xgm. + */ + @Test + public void crossDomainRedirectStripsCookieHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnA.set(null); + lastCookieHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setHeader("Cookie", "session=abc123; csrf=xyz789") + .execute() + .get(5, TimeUnit.SECONDS); + + // Cookie should be present on the original request to server A + assertEquals(lastCookieHeaderOnA.get(), "session=abc123; csrf=xyz789", + "Cookie header should be present on original request"); + // Cookie must NOT be forwarded to the cross-domain target (server B) + assertNull(lastCookieHeaderOnB.get(), + "Cookie header must be stripped on cross-domain redirect"); + } + } + + /** + * Same-origin redirect (same host and port) should preserve the Cookie header. + */ + @Test + public void sameOriginRedirectPreservesCookieHeader() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setHeader("Cookie", "session=abc123") + .execute() + .get(5, TimeUnit.SECONDS); + + assertEquals(lastCookieHeaderOnA.get(), "session=abc123", + "Cookie header should be preserved on same-origin redirect"); + } + } + + /** + * Cross-domain redirect must strip both Authorization and Cookie when both are set. + * Combined regression that mirrors the original PoC. + */ + @Test + public void crossDomainRedirectStripsBothCookieAndAuthorization() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnA.set(null); + lastAuthHeaderOnB.set(null); + lastCookieHeaderOnA.set(null); + lastCookieHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setHeader("Authorization", "Bearer token123") + .setHeader("Cookie", "session=abc123; api_key=secret") + .execute() + .get(5, TimeUnit.SECONDS); + + assertEquals(lastAuthHeaderOnA.get(), "Bearer token123", + "Authorization header should be present on original request"); + assertEquals(lastCookieHeaderOnA.get(), "session=abc123; api_key=secret", + "Cookie header should be present on original request"); + assertNull(lastAuthHeaderOnB.get(), + "Authorization header must be stripped on cross-domain redirect"); + assertNull(lastCookieHeaderOnB.get(), + "Cookie header must be stripped on cross-domain redirect"); + } + } + + /** + * Multi-hop: A -> A (same-origin, Cookie preserved) -> B (cross-domain, Cookie stripped). + */ + @Test + public void multiHopChainStripsCookieAtFirstCrossOriginHop() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + cookieAtChainStep2.set(null); + lastCookieHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/chain-same-then-cross") + .setHeader("Cookie", "session=abc123") + .execute() + .get(5, TimeUnit.SECONDS); + + // Cookie should survive the same-origin intermediate hop (A -> A) + assertEquals(cookieAtChainStep2.get(), "session=abc123", + "Cookie header should be preserved on same-origin intermediate redirect"); + // Cookie must be stripped on the cross-domain hop (A -> B) + assertNull(lastCookieHeaderOnB.get(), + "Cookie header must be stripped on cross-domain hop in redirect chain"); + } + } + + /** + * Once Cookie is stripped at a cross-domain hop, it must not reappear on subsequent hops. + */ + @Test + public void multiHopCookieStaysStrippedAfterCrossDomain() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + cookieOnBounceBack.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/chain-cross-and-back") + .setHeader("Cookie", "session=abc123") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(cookieOnBounceBack.get(), + "Cookie must not reappear after being stripped at a cross-domain hop"); + } + } + + /** + * setStripAuthorizationOnRedirect(true) also strips Cookie on same-origin redirects: + * the conditions are coupled, so users opting into strict credential stripping get cookie + * stripping on the same-origin path too. + */ + @Test + public void stripAuthorizationOnRedirectFlagAlsoStripsCookie() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setStripAuthorizationOnRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnA.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-same-origin") + .setHeader("Cookie", "session=abc123") + .execute() + .get(5, TimeUnit.SECONDS); + + // With stripAuthorizationOnRedirect=true, even same-origin redirects strip the Cookie + assertNull(lastCookieHeaderOnA.get(), + "stripAuthorizationOnRedirect=true must also strip Cookie on same-origin redirect"); + } + } + + /** + * Regression: a cookie added to the URI-scoped CookieStore for server B is still delivered + * to server B after a cross-origin redirect from A -> B. Cookie stripping must not break the + * legitimate cookie-store flow (store cookies are added after the strip step in + * Redirect30xInterceptor and are URI-matched). + */ + @Test + public void cookieStoreManagedCookiesUnaffectedByStrip() throws Exception { + ThreadSafeCookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://127.0.0.1:" + portB + "/target"), + new DefaultCookie("store_cookie", "store_value")); + + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setCookieStore(store) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastCookieHeaderOnB.set(null); + + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .execute() + .get(5, TimeUnit.SECONDS); + + // The store cookie scoped to server B's URI should reach server B + assertNotNull(lastCookieHeaderOnB.get(), + "URI-scoped CookieStore cookies should be delivered after cross-domain redirect"); + assertTrue(lastCookieHeaderOnB.get().contains("store_cookie=store_value"), + "Expected store cookie in Cookie header, got: " + lastCookieHeaderOnB.get()); + } + } + + /** + * Same host (127.0.0.1) on a different port is treated as cross-origin: both Cookie and + * Authorization are stripped. Locks in the port-as-part-of-origin behavior so that a future + * change to {@link Uri#isSameBase} cannot accidentally narrow the origin to host-only. + */ + @Test + public void portChangeOnSameHostIsTreatedAsCrossOrigin() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + lastAuthHeaderOnB.set(null); + lastCookieHeaderOnB.set(null); + + // /redirect-to-b: 127.0.0.1:portA -> 127.0.0.1:portB. Same host, different port. + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b") + .setHeader("Authorization", "Bearer same-host-token") + .setHeader("Cookie", "session=same-host-cookie") + .execute() + .get(5, TimeUnit.SECONDS); + + assertNull(lastAuthHeaderOnB.get(), + "Authorization must be stripped when only the port differs (origin includes port)"); + assertNull(lastCookieHeaderOnB.get(), + "Cookie must be stripped when only the port differs (origin includes port)"); + } + } } diff --git a/example/pom.xml b/example/pom.xml index 54a71ec105..cb705ba8e3 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.14.5 + 2.15.0 4.0.0 async-http-client-example diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index a95a27c47d..98f48cedbb 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.14.5 + 2.15.0 4.0.0 async-http-client-extras-guava diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index 1cfcfeb422..64d81e047b 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,7 +18,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.14.5 + 2.15.0 async-http-client-extras-jdeferred Asynchronous Http Client JDeferred Extras diff --git a/extras/pom.xml b/extras/pom.xml index 6ef4f73ab8..d735025314 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.14.5 + 2.15.0 4.0.0 async-http-client-extras-parent diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index 168464ac2c..70b832a334 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.14.5 + 2.15.0 4.0.0 async-http-client-extras-registry diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index 2e3811ce91..2cfbb2f077 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.14.5 + 2.15.0 async-http-client-extras-retrofit2 diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index 175778dcdb..e8a383ed5f 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.14.5 + 2.15.0 async-http-client-extras-rxjava Asynchronous Http Client RxJava Extras diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index 812ca16de1..a32efb15e6 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.14.5 + 2.15.0 async-http-client-extras-rxjava2 Asynchronous Http Client RxJava2 Extras diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index 615a5a2e0c..76f4f98cce 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.14.5 + 2.15.0 async-http-client-extras-simple Asynchronous Http Simple Client diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index fd2b2e2e32..8df1284558 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.14.5 + 2.15.0 async-http-client-extras-typesafe-config diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index d8950d171e..1b92154319 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.14.5 + 2.15.0 4.0.0 async-http-client-netty-utils diff --git a/pom.xml b/pom.xml index dbc1374370..bbe8075d50 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.asynchttpclient async-http-client-project - 2.14.5 + 2.15.0 pom Asynchronous Http Client Project @@ -34,7 +34,7 @@ scm:git:git@github.com:AsyncHttpClient/async-http-client.git scm:git:git@github.com:AsyncHttpClient/async-http-client.git https://github.com/AsyncHttpClient/async-http-client/tree/master - async-http-client-project-2.14.5 + async-http-client-project-2.15.0 From 9e52c0ca7cc10d8d19fc77a460e6e016f14a90d9 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Tue, 12 May 2026 20:34:18 +0000 Subject: [PATCH 04/32] chore: Dependency upgrade --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bbe8075d50..6672bf0493 100644 --- a/pom.xml +++ b/pom.xml @@ -462,7 +462,7 @@ true 1.8 1.8 - 4.1.121.Final + 4.1.133.Final 1.7.36 1.0.4 1.2.2 From 8e4069cf3c92abe099db5fb13378ac2fe9e1fd3b Mon Sep 17 00:00:00 2001 From: Aayush Atharva <24762260+hyperxpro@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:16:45 +0530 Subject: [PATCH 05/32] Backport: reject set-cookie domain that doesn't match the request host (#2199) Backport of: https://github.com/AsyncHttpClient/async-http-client/pull/2196 --- bom/pom.xml | 2 +- client/pom.xml | 2 +- .../cookie/ThreadSafeCookieStore.java | 13 +++++++++++++ .../java/org/asynchttpclient/CookieStoreTest.java | 9 +++++++++ example/pom.xml | 2 +- extras/guava/pom.xml | 2 +- extras/jdeferred/pom.xml | 2 +- extras/pom.xml | 2 +- extras/registry/pom.xml | 2 +- extras/retrofit2/pom.xml | 2 +- extras/rxjava/pom.xml | 2 +- extras/rxjava2/pom.xml | 2 +- extras/simple/pom.xml | 2 +- extras/typesafeconfig/pom.xml | 2 +- netty-utils/pom.xml | 2 +- pom.xml | 4 ++-- 16 files changed, 37 insertions(+), 15 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index c66b2fae7b..c135bd59d1 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ org.asynchttpclient async-http-client-project - 2.15.0 + 2.16.0 async-http-client-bom diff --git a/client/pom.xml b/client/pom.xml index 0dcb45c1bd..a1f941250e 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.15.0 + 2.16.0 4.0.0 async-http-client diff --git a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java index 8cdc29f45e..9e2e6a658d 100644 --- a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java +++ b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java @@ -154,6 +154,11 @@ private boolean hasCookieExpired(Cookie cookie, long whenCreated) { return false; } + // rfc6265#section-5.1.3 + private boolean domainsMatch(String cookieDomain, String requestDomain) { + return requestDomain.equals(cookieDomain) || requestDomain.endsWith('.' + cookieDomain); + } + // rfc6265#section-5.1.4 private boolean pathsMatch(String cookiePath, String requestPath) { return Objects.equals(cookiePath, requestPath) || @@ -164,6 +169,14 @@ private void add(String requestDomain, String requestPath, Cookie cookie) { AbstractMap.SimpleEntry pair = cookieDomain(cookie.domain(), requestDomain); String keyDomain = pair.getKey(); boolean hostOnly = pair.getValue(); + + // rfc6265#section-5.3 step 6: ignore a cookie whose Domain attribute is not + // domain-matched by the request host, otherwise a host can plant cookies for + // unrelated domains (cookie tossing). + if (!hostOnly && !domainsMatch(keyDomain, requestDomain)) { + return; + } + String keyPath = cookiePath(cookie.path(), requestPath); CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyPath); diff --git a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java index d97276971c..c5ad915487 100644 --- a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java +++ b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java @@ -55,6 +55,7 @@ public void tearDownGlobal() { public void runAllSequentiallyBecauseNotThreadSafe() throws Exception { addCookieWithEmptyPath(); dontReturnCookieForAnotherDomain(); + dontStoreCookieForUnrelatedDomainAttribute(); returnCookieWhenItWasSetOnSamePath(); returnCookieWhenItWasSetOnParentPath(); dontReturnCookieWhenDomainMatchesButPathIsDifferent(); @@ -93,6 +94,14 @@ private void addCookieWithEmptyPath() { assertTrue(store.get(uri).size() > 0); } + // rfc6265#section-5.3 step 6: a host must not be able to set a cookie for an unrelated domain + private void dontStoreCookieForUnrelatedDomainAttribute() { + CookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("http://www.evil.com/"), ClientCookieDecoder.LAX.decode("SID=attacker; Domain=victim.com")); + assertTrue(store.get(Uri.create("https://victim.com/account")).isEmpty()); + assertTrue(store.getAll().isEmpty()); + } + private void dontReturnCookieForAnotherDomain() { CookieStore store = new ThreadSafeCookieStore(); store.add(Uri.create("http://www.foo.com"), ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; path=")); diff --git a/example/pom.xml b/example/pom.xml index cb705ba8e3..d88037ca0f 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.15.0 + 2.16.0 4.0.0 async-http-client-example diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index 98f48cedbb..8e5f62c720 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.15.0 + 2.16.0 4.0.0 async-http-client-extras-guava diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index 64d81e047b..80fbd9bd44 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,7 +18,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.15.0 + 2.16.0 async-http-client-extras-jdeferred Asynchronous Http Client JDeferred Extras diff --git a/extras/pom.xml b/extras/pom.xml index d735025314..006534c4ca 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.15.0 + 2.16.0 4.0.0 async-http-client-extras-parent diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index 70b832a334..79a52a656e 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.15.0 + 2.16.0 4.0.0 async-http-client-extras-registry diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index 2cfbb2f077..c91daa9a54 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.15.0 + 2.16.0 async-http-client-extras-retrofit2 diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index e8a383ed5f..8fb7cf99a5 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.15.0 + 2.16.0 async-http-client-extras-rxjava Asynchronous Http Client RxJava Extras diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index a32efb15e6..c1bce44e1c 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.15.0 + 2.16.0 async-http-client-extras-rxjava2 Asynchronous Http Client RxJava2 Extras diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index 76f4f98cce..5fd546ef95 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.15.0 + 2.16.0 async-http-client-extras-simple Asynchronous Http Simple Client diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index 8df1284558..5c5a8fd2fe 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.15.0 + 2.16.0 async-http-client-extras-typesafe-config diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index 1b92154319..41df40b3bb 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.15.0 + 2.16.0 4.0.0 async-http-client-netty-utils diff --git a/pom.xml b/pom.xml index 6672bf0493..b03b29a5b9 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.asynchttpclient async-http-client-project - 2.15.0 + 2.16.0 pom Asynchronous Http Client Project @@ -34,7 +34,7 @@ scm:git:git@github.com:AsyncHttpClient/async-http-client.git scm:git:git@github.com:AsyncHttpClient/async-http-client.git https://github.com/AsyncHttpClient/async-http-client/tree/master - async-http-client-project-2.15.0 + async-http-client-project-2.16.0 From 9330443412aa53e43a050b02e586d72bd592da8b Mon Sep 17 00:00:00 2001 From: joachimmaes Date: Fri, 17 Jul 2026 21:24:40 +0200 Subject: [PATCH 06/32] Upgrade Netty 4.1.136.Final (#2238) Netty 4.1.133.Final is affected by GHSA-hvcg-qmg6-jm4c (netty-codec-http: HttpObjectDecoder skips arbitrary initial control characters) and GHSA-3qp7-7mw8-wx86 (netty-handler: IPv6 subnet filter bypass), both fixed in 4.1.135.Final. The main branch already moved to the fixed 4.2.15.Final; this brings the 2.x line to the current 4.1.x patch level. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b03b29a5b9..7fc485cf09 100644 --- a/pom.xml +++ b/pom.xml @@ -462,7 +462,7 @@ true 1.8 1.8 - 4.1.133.Final + 4.1.136.Final 1.7.36 1.0.4 1.2.2 From 9dba5ac988b7e750551f59b2eab550b60ac8a0d6 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:07:35 +0000 Subject: [PATCH 07/32] Stop origin credentials leaking to the proxy on CONNECT Motivation: When a request to an https:// (or ws:///wss://) origin goes through an HTTP proxy, AHC first sends a CONNECT to the proxy in the clear to open the tunnel. Both preemptive-auth sinks attached the origin Authorization header unconditionally: NettyRequestFactory.newNettyRequest called addAuthorizationHeader for every request it built including the CONNECT, and NettyRequestSender.writeRequest did the same for the per-connection NTLM/Kerberos/SPNEGO realms. The origin's Basic/Digest/NTLM credentials therefore travelled to the proxy on a plaintext hop the origin never sees, and any on-path observer of that hop sees them too. Modification: Gate both sinks on the request not being a CONNECT. The factory skips addAuthorizationHeader when connect is true; the sender skips perConnectionAuthorizationHeader when the netty request's method is CONNECT. The tunneled request is built separately once the tunnel is up and still carries the origin credentials, so nothing is lost. The CONNECT's own Proxy-Authorization is untouched: the proxy is the intended recipient of that one. Result: A CONNECT no longer exposes origin credentials to the proxy, while the request sent through the tunnel is unchanged. Added ConnectRequestAuthorizationTest. --- .../netty/request/NettyRequestFactory.java | 8 +- .../netty/request/NettyRequestSender.java | 8 +- .../ConnectRequestAuthorizationTest.java | 76 +++++++++++++++++++ 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/netty/request/ConnectRequestAuthorizationTest.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 4cfee06cd6..f90e469a3a 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java @@ -199,8 +199,12 @@ public NettyRequest newNettyRequest(Request request, boolean performConnectReque headers.set(HOST, virtualHost != null ? virtualHost : hostHeader(uri)); } - // don't override authorization but append - addAuthorizationHeader(headers, perRequestAuthorizationHeader(request, realm)); + // don't override authorization but append. Skip it on a CONNECT: that request is sent to the + // proxy in the clear to open the tunnel, so the origin Authorization would be exposed to the + // proxy. It is added to the tunneled request, which is built separately once the tunnel is up. + if (!connect) { + addAuthorizationHeader(headers, perRequestAuthorizationHeader(request, realm)); + } // only set proxy auth on request over plain HTTP, or when performing CONNECT if (!uri.isSecured() || connect) { setProxyAuthorizationHeader(headers, perRequestProxyAuthorizationHeader(request, proxyRealm)); diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index aed08b7a70..a78c07d8ec 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -283,7 +283,13 @@ private ListenableFuture sendRequestWithNewChannel(Request request, } Realm realm = future.getRealm(); Realm proxyRealm = future.getProxyRealm(); - requestFactory.addAuthorizationHeader(headers, perConnectionAuthorizationHeader(request, proxy, realm)); + // On the tunnel path this is the CONNECT request, sent to the proxy in the clear before the TLS + // tunnel exists. Preemptive NTLM/Kerberos/SPNEGO realms attach their header here rather than in + // the factory, so skip it on CONNECT to keep the origin credentials off the plaintext hop. They + // travel on the tunneled request once the tunnel is up. + if (future.getNettyRequest().getHttpRequest().method() != HttpMethod.CONNECT) { + requestFactory.addAuthorizationHeader(headers, perConnectionAuthorizationHeader(request, proxy, realm)); + } requestFactory.setProxyAuthorizationHeader(headers, perConnectionProxyAuthorizationHeader(request, proxyRealm)); future.setInAuth(realm != null && realm.isUsePreemptiveAuth() && realm.getScheme() != AuthScheme.NTLM); diff --git a/client/src/test/java/org/asynchttpclient/netty/request/ConnectRequestAuthorizationTest.java b/client/src/test/java/org/asynchttpclient/netty/request/ConnectRequestAuthorizationTest.java new file mode 100644 index 0000000000..4743da73d9 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/ConnectRequestAuthorizationTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.request; + +import io.netty.handler.codec.http.HttpHeaderNames; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.testng.annotations.Test; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A CONNECT request opens the proxy tunnel and is sent to the proxy in the clear, so it must not carry the + * origin {@code Authorization} header (which would expose the origin credentials to the proxy). The header + * belongs only on the request sent through the established tunnel. + */ +public class ConnectRequestAuthorizationTest { + + private static NettyRequestFactory factory() { + return new NettyRequestFactory(config().build()); + } + + @Test + public void connectRequestDoesNotCarryOriginAuthorization() { + Request request = get("https://origin.example.com/resource").build(); + Realm realm = basicAuthRealm("user", "secret").setUsePreemptiveAuth(true).build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest connect = factory().newNettyRequest(request, true, proxy, realm, null); + + assertFalse(connect.getHttpRequest().headers().contains(HttpHeaderNames.AUTHORIZATION), + "CONNECT request must not expose the origin Authorization to the proxy"); + } + + @Test + public void tunneledRequestKeepsOriginAuthorization() { + Request request = get("https://origin.example.com/resource").build(); + Realm realm = basicAuthRealm("user", "secret").setUsePreemptiveAuth(true).build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest tunneled = factory().newNettyRequest(request, false, proxy, realm, null); + + assertTrue(tunneled.getHttpRequest().headers().contains(HttpHeaderNames.AUTHORIZATION), + "the request sent through the tunnel must still carry the origin Authorization"); + } + + @Test + public void connectRequestDoesNotCarryOriginAuthorizationSetAsAPlainHeader() { + Request request = get("https://origin.example.com/resource") + .setHeader(HttpHeaderNames.AUTHORIZATION, "Bearer origin-token") + .build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest connect = factory().newNettyRequest(request, true, proxy, null, null); + + assertFalse(connect.getHttpRequest().headers().contains(HttpHeaderNames.AUTHORIZATION), + "an explicitly set origin Authorization must not ride the plaintext CONNECT either"); + } +} From 718ed2e86126663a88ea3db7a88cbbf93f0069b5 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:09:44 +0000 Subject: [PATCH 08/32] Stop proxy credentials leaking to the origin over a SOCKS proxy Motivation: Both proxy-auth attach sinks assumed an HTTP proxy. For a SOCKS proxy the request is tunnelled at the transport layer and reaches the ORIGIN, so attaching a Proxy-Authorization header exposed the proxy credentials to the origin server. NettyRequestFactory.newNettyRequest keyed its gate only off uri.isSecured()/connect, and NettyRequestSender.writeRequest attached the per-connection NTLM/Kerberos/SPNEGO proxy header with no gate at all. Neither ever receives a CONNECT for a SOCKS proxy, since needConnect already requires getProxyType().isHttp(). Modification: Guard both sinks on proxyServer.getProxyType().isHttp(), the same test requestUri() already uses when it decides to emit an absolute-form request target. A null proxy now also short-circuits the per-request gate, where before a stray proxy realm could set the header with no proxy configured. Result: A request routed through a SOCKS proxy no longer carries the proxy's credentials to the origin. HTTP-proxy behaviour, including the CONNECT, is unchanged. Added SocksProxyCredentialLeakTest. --- .../netty/request/NettyRequestFactory.java | 7 +- .../netty/request/NettyRequestSender.java | 8 +- .../request/SocksProxyCredentialLeakTest.java | 130 ++++++++++++++++++ 3 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/netty/request/SocksProxyCredentialLeakTest.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 f90e469a3a..f500513324 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java @@ -205,8 +205,11 @@ public NettyRequest newNettyRequest(Request request, boolean performConnectReque if (!connect) { addAuthorizationHeader(headers, perRequestAuthorizationHeader(request, realm)); } - // only set proxy auth on request over plain HTTP, or when performing CONNECT - if (!uri.isSecured() || connect) { + // Only set proxy auth on a request sent to an HTTP proxy: either over plain HTTP (the origin request + // carries an absolute URI straight to the proxy) or on a CONNECT (sent to the proxy to open the + // tunnel). A SOCKS proxy tunnels at the transport layer, so the request reaches the ORIGIN - a + // Proxy-Authorization header would leak the proxy credentials to the origin. Guard on the proxy type. + if ((connect || !uri.isSecured()) && proxyServer != null && proxyServer.getProxyType().isHttp()) { setProxyAuthorizationHeader(headers, perRequestProxyAuthorizationHeader(request, proxyRealm)); } diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index a78c07d8ec..f2a8038d77 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -290,7 +290,13 @@ private ListenableFuture sendRequestWithNewChannel(Request request, if (future.getNettyRequest().getHttpRequest().method() != HttpMethod.CONNECT) { requestFactory.addAuthorizationHeader(headers, perConnectionAuthorizationHeader(request, proxy, realm)); } - requestFactory.setProxyAuthorizationHeader(headers, perConnectionProxyAuthorizationHeader(request, proxyRealm)); + // Preemptive per-connection proxy auth (NTLM/Kerberos/SPNEGO) belongs only on a request sent to an + // HTTP proxy. Over a SOCKS proxy the transport tunnels this request to the ORIGIN, so attaching + // Proxy-Authorization here would leak the proxy credentials to the origin. Guard on the proxy type, + // mirroring the per-request gate in NettyRequestFactory. + if (proxy != null && proxy.getProxyType().isHttp()) { + requestFactory.setProxyAuthorizationHeader(headers, perConnectionProxyAuthorizationHeader(request, proxyRealm)); + } future.setInAuth(realm != null && realm.isUsePreemptiveAuth() && realm.getScheme() != AuthScheme.NTLM); future.setInProxyAuth( diff --git a/client/src/test/java/org/asynchttpclient/netty/request/SocksProxyCredentialLeakTest.java b/client/src/test/java/org/asynchttpclient/netty/request/SocksProxyCredentialLeakTest.java new file mode 100644 index 0000000000..f33f9dc362 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/SocksProxyCredentialLeakTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.request; + +import io.netty.handler.codec.http.HttpHeaderNames; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.asynchttpclient.proxy.ProxyType; +import org.testng.annotations.Test; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A SOCKS proxy tunnels at the transport layer, so the HTTP request it carries reaches the ORIGIN, not the + * proxy. Attaching a {@code Proxy-Authorization} header for a SOCKS proxy therefore leaks the proxy + * credentials to the origin server. The origin request must carry the header only for an HTTP proxy. + */ +public class SocksProxyCredentialLeakTest { + + private static NettyRequestFactory factory() { + return new NettyRequestFactory(config().build()); + } + + private static Realm preemptiveBasicProxyRealm() { + return basicAuthRealm("proxy-user", "proxy-secret").setUsePreemptiveAuth(true).build(); + } + + private static ProxyServer proxy(ProxyType type, Realm proxyRealm) { + return proxyServer("proxy.example.com", 1080).setProxyType(type).setRealm(proxyRealm).build(); + } + + private static boolean hasProxyAuthorization(NettyRequest nettyRequest) { + return nettyRequest.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION); + } + + @Test + public void socks5ProxyDoesNotAttachProxyAuthorizationToPlaintextOrigin() { + Request request = get("http://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + ProxyServer socksProxy = proxy(ProxyType.SOCKS_V5, proxyRealm); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, socksProxy, null, proxyRealm); + + assertFalse(hasProxyAuthorization(nettyRequest), + "SOCKS proxy tunnels to the origin; Proxy-Authorization must not be sent to the origin"); + } + + @Test + public void socks4ProxyDoesNotAttachProxyAuthorizationToPlaintextOrigin() { + Request request = get("http://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + ProxyServer socksProxy = proxy(ProxyType.SOCKS_V4, proxyRealm); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, socksProxy, null, proxyRealm); + + assertFalse(hasProxyAuthorization(nettyRequest), + "SOCKS proxy tunnels to the origin; Proxy-Authorization must not be sent to the origin"); + } + + @Test + public void socks5ProxyDoesNotAttachProxyAuthorizationToSecuredOrigin() { + // A https:// origin behind a SOCKS proxy never gets a CONNECT (needConnect requires an HTTP proxy), + // so the request itself is tunnelled straight to the origin. + Request request = get("https://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + ProxyServer socksProxy = proxy(ProxyType.SOCKS_V5, proxyRealm); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, socksProxy, null, proxyRealm); + + assertFalse(hasProxyAuthorization(nettyRequest), + "SOCKS proxy tunnels to the origin; Proxy-Authorization must not be sent to the origin"); + } + + @Test + public void httpProxyStillAttachesProxyAuthorizationToPlaintextOrigin() { + Request request = get("http://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + // Default proxy type is HTTP. + ProxyServer httpProxy = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, httpProxy, null, proxyRealm); + + assertTrue(hasProxyAuthorization(nettyRequest), + "HTTP proxy receives the origin request directly; Proxy-Authorization must be sent"); + assertTrue(nettyRequest.getHttpRequest().headers().get(HttpHeaderNames.PROXY_AUTHORIZATION).startsWith("Basic "), + "expected a preemptive Basic Proxy-Authorization header"); + } + + @Test + public void connectRequestThroughHttpProxyStillCarriesProxyAuthorization() { + // A CONNECT to open a tunnel for an HTTPS origin is sent to the proxy in the clear, so it legitimately + // carries the proxy credentials. + Request request = get("https://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + ProxyServer httpProxy = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + + NettyRequest connect = factory().newNettyRequest(request, true, httpProxy, null, proxyRealm); + + assertTrue(hasProxyAuthorization(connect), + "CONNECT is sent to the proxy to open the tunnel; Proxy-Authorization must be sent"); + } + + @Test + public void directRequestWithoutAProxyCarriesNoProxyAuthorization() { + Request request = get("http://origin.example.com/resource").build(); + Realm proxyRealm = preemptiveBasicProxyRealm(); + + NettyRequest nettyRequest = factory().newNettyRequest(request, false, null, null, proxyRealm); + + assertFalse(hasProxyAuthorization(nettyRequest), + "there is no proxy to authenticate to"); + } +} From c4feab0f7f86d61505a48e40d383c8a375a22e18 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:13:38 +0000 Subject: [PATCH 09/32] Stop proxy credentials leaking to origin on ws:// tunnel Motivation: A ws:// request through an HTTP proxy is tunnelled with CONNECT (the needConnect check in NettyRequestSender treats ws:// like https://), so the upgrade request that follows travels through the established tunnel straight to the origin, not to the proxy. newNettyRequest's proxy-auth gate and requestUri's request-target selection both keyed only off uri.isSecured(), which is false for ws://, so the tunnelled upgrade request incorrectly carried the proxy's Proxy-Authorization header and an absolute-form request target meant for the proxy. Modification: Exclude WebSocket URIs from both sites, mirroring how wss:// is already handled, alongside the existing SOCKS getProxyType().isHttp() guard on the same two conditions: the CONNECT still authenticates to the proxy, the tunnelled upgrade request no longer carries proxy credentials, and its request target is origin-form. Result: A ws:// request behind an HTTP proxy no longer exposes the proxy's Basic/Digest credentials to the origin server. Added WebSocketTunnelProxyAuthTest covering CONNECT, the tunnelled upgrade, and the unaffected plain-HTTP and wss:// paths. --- .../netty/request/NettyRequestFactory.java | 10 +- .../request/WebSocketTunnelProxyAuthTest.java | 93 +++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/netty/request/WebSocketTunnelProxyAuthTest.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 f500513324..5d6790e2d3 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java @@ -209,7 +209,10 @@ public NettyRequest newNettyRequest(Request request, boolean performConnectReque // carries an absolute URI straight to the proxy) or on a CONNECT (sent to the proxy to open the // tunnel). A SOCKS proxy tunnels at the transport layer, so the request reaches the ORIGIN - a // Proxy-Authorization header would leak the proxy credentials to the origin. Guard on the proxy type. - if ((connect || !uri.isSecured()) && proxyServer != null && proxyServer.getProxyType().isHttp()) { + // A ws:// request is tunnelled through CONNECT the same way wss:// is (see NettyRequestSender's + // needConnect check), so the upgrade request that follows also reaches the origin, not the proxy; + // exclude it from the plain-HTTP branch the same way wss:// already is. + if ((connect || (!uri.isSecured() && !uri.isWebSocket())) && proxyServer != null && proxyServer.getProxyType().isHttp()) { setProxyAuthorizationHeader(headers, perRequestProxyAuthorizationHeader(request, proxyRealm)); } @@ -231,8 +234,9 @@ private String requestUri(Uri uri, ProxyServer proxyServer, boolean connect) { // proxy tunnelling, connect need host and explicit port return uri.getAuthority(); - } else if (proxyServer != null && !uri.isSecured() && proxyServer.getProxyType().isHttp()) { - // proxy over HTTP, need full url + } else if (proxyServer != null && !uri.isSecured() && !uri.isWebSocket() && proxyServer.getProxyType().isHttp()) { + // proxy over HTTP, need full url. A ws:// request is tunnelled through CONNECT, so its upgrade + // request reaches the origin and takes the origin-form request target below return uri.toUrl(); } else { diff --git a/client/src/test/java/org/asynchttpclient/netty/request/WebSocketTunnelProxyAuthTest.java b/client/src/test/java/org/asynchttpclient/netty/request/WebSocketTunnelProxyAuthTest.java new file mode 100644 index 0000000000..a27db86ae6 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/WebSocketTunnelProxyAuthTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.request; + +import io.netty.handler.codec.http.HttpHeaderNames; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.testng.annotations.Test; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A ws:// request through an HTTP proxy is tunnelled with CONNECT (NettyRequestSender's needConnect check + * treats ws:// like https://), so the upgrade request that follows travels through the tunnel to the origin, + * not to the proxy. It must therefore be treated like wss://: the proxy credentials belong only on the + * CONNECT (which the proxy actually receives), and the request target must be origin-form. + */ +public class WebSocketTunnelProxyAuthTest { + + private static NettyRequestFactory factory() { + return new NettyRequestFactory(config().build()); + } + + private static Realm preemptiveBasicProxyRealm() { + return basicAuthRealm("proxy-user", "proxy-secret").setUsePreemptiveAuth(true).build(); + } + + @Test + public void connectRequestForWebSocketCarriesProxyAuthorization() { + Request request = get("ws://origin.example.com/socket").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest connect = factory().newNettyRequest(request, true, proxy, null, preemptiveBasicProxyRealm()); + + assertTrue(connect.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION), + "the CONNECT request must still authenticate to the proxy"); + } + + @Test + public void tunneledWebSocketUpgradeDoesNotCarryProxyAuthorization() { + Request request = get("ws://origin.example.com/socket").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest tunneled = factory().newNettyRequest(request, false, proxy, null, preemptiveBasicProxyRealm()); + + assertFalse(tunneled.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION), + "the tunnelled ws:// upgrade reaches the origin, not the proxy, and must not expose the proxy credentials"); + assertEquals(tunneled.getHttpRequest().uri(), "/socket", + "the tunnelled ws:// upgrade must use an origin-form request target, not the proxy's absolute-form"); + } + + @Test + public void tunneledSecureWebSocketUpgradeDoesNotCarryProxyAuthorization() { + Request request = get("wss://origin.example.com/socket").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest tunneled = factory().newNettyRequest(request, false, proxy, null, preemptiveBasicProxyRealm()); + + assertFalse(tunneled.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION), + "wss:// was already tunnel-safe; this guards against a regression"); + } + + @Test + public void plainHttpRequestToProxyKeepsProxyAuthorization() { + Request request = get("http://origin.example.com/resource").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest direct = factory().newNettyRequest(request, false, proxy, null, preemptiveBasicProxyRealm()); + + assertTrue(direct.getHttpRequest().headers().contains(HttpHeaderNames.PROXY_AUTHORIZATION), + "a plaintext http:// request sent directly to the proxy still authenticates to it"); + assertEquals(direct.getHttpRequest().uri(), "http://origin.example.com/resource", + "a plaintext http:// request to the proxy still uses an absolute-form request target"); + } +} From 217bc83ca31fd330d775c48887b619adaa9b3a7b Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:15:45 +0000 Subject: [PATCH 10/32] Strip userinfo from the absolute-form proxy request target Motivation: A plaintext request routed through an HTTP proxy is sent with an absolute-form request target built by Uri.toUrl(), which embeds the deprecated userinfo subcomponent. Credentials carried in the URL (http://user:secret@host/path) therefore appeared on the request line sent to the proxy in the clear, where they land in proxy access logs and in the hands of anyone on that hop. RFC 9110 section 4.2.4 says a sender MUST NOT generate the userinfo subcomponent when a request target is generated. Modification: Add Uri.toUrlWithoutUserInfo(), the same rendering as toUrl() minus "userinfo@", and use it in NettyRequestFactory.requestUri() for the proxy absolute-form branch. toUrl() itself is unchanged so the caller-visible URL, redirect resolution and cookie handling keep the userinfo. When there is no userinfo the new method delegates to the cached toUrl(), so the common path allocates nothing extra. Result: URL credentials are no longer exposed to an HTTP proxy on the request line. Added ProxyRequestUriUserInfoTest and Uri coverage for the new method. --- .../netty/request/NettyRequestFactory.java | 8 +- .../java/org/asynchttpclient/uri/Uri.java | 24 ++++++ .../request/ProxyRequestUriUserInfoTest.java | 78 +++++++++++++++++++ .../java/org/asynchttpclient/uri/UriTest.java | 16 ++++ 4 files changed, 123 insertions(+), 3 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 5d6790e2d3..8556cd5d14 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java @@ -235,9 +235,11 @@ private String requestUri(Uri uri, ProxyServer proxyServer, boolean connect) { return uri.getAuthority(); } else if (proxyServer != null && !uri.isSecured() && !uri.isWebSocket() && proxyServer.getProxyType().isHttp()) { - // proxy over HTTP, need full url. A ws:// request is tunnelled through CONNECT, so its upgrade - // request reaches the origin and takes the origin-form request target below - 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 section 4.2.4 forbids userinfo in a generated request target. A ws:// request is + // tunnelled through CONNECT, so its upgrade request reaches the origin and takes the origin-form + // request target below + 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 19986dcfc3..580c495a73 100644 --- a/client/src/main/java/org/asynchttpclient/uri/Uri.java +++ b/client/src/main/java/org/asynchttpclient/uri/Uri.java @@ -149,6 +149,30 @@ public String toUrl() { return url; } + /** + * Same as {@link #toUrl()} but without the deprecated userinfo subcomponent. RFC 9110 section 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 0000000000..c115516165 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/request/ProxyRequestUriUserInfoTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.request; + +import org.asynchttpclient.Request; +import org.asynchttpclient.proxy.ProxyServer; +import org.testng.annotations.Test; + +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.Dsl.get; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.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 section 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(requestTarget, "http://origin.example.com/resource?a=b"); + } + + @Test + public void proxiedRequestTargetDropsUserInfoOnAnExplicitPort() { + Request request = get("http://user:secret@origin.example.com:8081/resource").build(); + ProxyServer proxy = proxyServer("proxy.example.com", 8080).build(); + + NettyRequest proxied = factory().newNettyRequest(request, false, proxy, null, null); + + assertEquals(proxied.getHttpRequest().uri(), "http://origin.example.com:8081/resource"); + } + + @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(proxied.getHttpRequest().uri(), "http://origin.example.com/resource?a=b"); + } + + @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(direct.getHttpRequest().uri(), "/resource?a=b"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/uri/UriTest.java b/client/src/test/java/org/asynchttpclient/uri/UriTest.java index 7ead2a6528..1d4bf41789 100644 --- a/client/src/test/java/org/asynchttpclient/uri/UriTest.java +++ b/client/src/test/java/org/asynchttpclient/uri/UriTest.java @@ -137,6 +137,22 @@ public void testToUrlWithUserInfoPortPathAndQuery() { assertEquals(uri.toUrl(), "http://user@example.com:44/path/path2?query=4", "toUrl returned incorrect url"); } + @Test + public void testToUrlWithoutUserInfoDropsUserInfo() { + Uri uri = new Uri("http", "user:secret", "example.com", 44, "/path/path2", "query=4", null); + assertEquals(uri.toUrlWithoutUserInfo(), "http://example.com:44/path/path2?query=4", + "toUrlWithoutUserInfo must not emit the userinfo subcomponent"); + assertEquals(uri.toUrl(), "http://user:secret@example.com:44/path/path2?query=4", + "toUrl must keep the userinfo for the caller-visible URL"); + } + + @Test + public void testToUrlWithoutUserInfoWithoutUserInfoMatchesToUrl() { + Uri uri = new Uri("http", null, "example.com", -1, "/path", "query=4", null); + assertEquals(uri.toUrlWithoutUserInfo(), uri.toUrl(), + "toUrlWithoutUserInfo must be identical to toUrl when there is no userinfo"); + } + @Test public void testQueryWithNonRootPath() { Uri uri = Uri.create("http://hello.com/foo?query=value"); From 43db7bba81430cbd61ec2dc2c7be464e0ff6a0ff Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:17:55 +0000 Subject: [PATCH 11/32] Stop the config realm leaking to a cross-origin redirect target Motivation: Redirect30xInterceptor strips credentials on a cross-origin or scheme-downgrade redirect: it drops the request realm and clears the realm on the future. But exitAfterIntercept re-derived the realm as "request realm, else config realm", so a client-wide realm configured via config.setRealm(...) was re-attached on the very next response. A redirect target on a different origin that answers 401 then received the configured credentials, which is exactly what the strip existed to prevent. Modification: Read the realm from the future, which NettyRequestSender seeds from the request or the config when the exchange starts and Redirect30xInterceptor resets to null on the strip. There is no longer a second, unstripped source of credentials at 401-handling time. Result: A cross-domain redirect to a 401 target no longer receives the client-wide credentials. Normal preemptive and challenge-response auth is unaffected, since the future carries the same realm the old expression derived. Added a cross-origin 401 case to RedirectCredentialSecurityTest. --- .../netty/handler/intercept/Interceptors.java | 6 ++- .../RedirectCredentialSecurityTest.java | 52 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java index 134213f60a..d0d6dc71fe 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java @@ -67,7 +67,11 @@ public boolean exitAfterIntercept(Channel channel, ProxyServer proxyServer = future.getProxyServer(); int statusCode = response.status().code(); Request request = future.getCurrentRequest(); - Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm(); + // The realm carried by the exchange (seeded from the request or the config when the exchange + // started, and reset to null by Redirect30xInterceptor on a cross-origin or scheme-downgrade + // redirect). Re-deriving from config.getRealm() here re-attaches the client-wide credentials to a + // redirect target whose auth was just stripped, leaking them to a different origin that answers 401. + Realm realm = future.getRealm(); // This MUST BE called before Redirect30xInterceptor because latter assumes cookie store is already updated CookieStore cookieStore = config.getCookieStore(); diff --git a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java index 31b50e1a10..0f36aed467 100644 --- a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java +++ b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java @@ -27,6 +27,7 @@ import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -63,6 +64,7 @@ public class RedirectCredentialSecurityTest { private static final AtomicReference lastCookieHeaderOnB = new AtomicReference<>(); private static final AtomicReference cookieAtChainStep2 = new AtomicReference<>(); private static final AtomicReference cookieOnBounceBack = new AtomicReference<>(); + private static final AtomicReference authOn401Target = new AtomicReference<>(); @BeforeClass public static void startServers() throws Exception { @@ -192,6 +194,27 @@ public static void startServers() throws Exception { exchange.close(); }); + // Cross-domain redirect to a target that answers 401: the target must never receive + // credentials, even those configured client-wide via config.setRealm(...). + serverA.createContext("/redirect-to-b-401", exchange -> { + exchange.getResponseHeaders().add("Location", "http://127.0.0.1:" + portB + "/target-401"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + }); + + serverB.createContext("/target-401", exchange -> { + String auth = exchange.getRequestHeaders().getFirst("Authorization"); + if (auth != null) { + authOn401Target.set(auth); + } + exchange.getResponseHeaders().add("WWW-Authenticate", "Basic realm=\"target\""); + // A body-carrying response keeps the connection alive, so the client's authenticated retry (the + // leak this test guards against) actually reaches this handler instead of dying on a closed socket. + exchange.sendResponseHeaders(401, 0); + exchange.getResponseBody().close(); + exchange.close(); + }); + serverA.start(); serverB.start(); serverC.start(); @@ -665,4 +688,33 @@ public void portChangeOnSameHostIsTreatedAsCrossOrigin() throws Exception { "Cookie must be stripped when only the port differs (origin includes port)"); } } + + /** + * Client-wide credentials set via {@code config.setRealm(...)} must not be sent to a cross-domain + * redirect target, even when that target answers 401 to solicit them. The redirect clears the + * request/future realm, but the config realm must not be re-applied. + */ + @Test + public void crossDomainRedirectTo401TargetDoesNotLeakConfigRealm() throws Exception { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(true) + .setRealm(basicAuthRealm("user", "password").build()) + .build(); + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + authOn401Target.set(null); + + try { + client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b-401") + .execute() + .get(5, TimeUnit.SECONDS); + } catch (ExecutionException ignored) { + // Without the fix the 401 triggers an authenticated retry and the JDK test server closes the + // connection on it. What the request ends up returning is irrelevant here: the assertion below + // is the contract, and it must hold whether or not the exchange completed. + } + + assertNull(authOn401Target.get(), + "client-wide config Realm must not be sent to a cross-domain 401 target after redirect"); + } + } } From 75a278550aa9a980009d022fb4e635f9c8738c03 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:22:48 +0000 Subject: [PATCH 12/32] Fail the WebSocket handshake on an invalid Sec-WebSocket-Accept Motivation: RFC 6455 section 4.1 requires the client to fail the connection when the server's Sec-WebSocket-Accept does not match the base64 SHA-1 of the nonce it sent. WebSocketHandler detected the mismatch and called requestSender.abort, but there was no return: execution fell straight through and the pipeline was upgraded to WebSocket frames, a NettyWebSocket was handed to the handler and onOpen was fired. A server that cannot prove it saw the client's Sec-WebSocket-Key - a cache, a transparent intermediary or an attacker replaying a canned 101 - was therefore treated as a live peer, which is exactly the cross-protocol confusion the accept check exists to prevent. Modification: Return after aborting, so the mismatch path stops before the pipeline upgrade, like the invalid-status/upgrade/connection path just above it already does. Result: A bad Sec-WebSocket-Accept now fails the request and never opens a WebSocket. Added WebSocketHandshakeValidationTest, which serves a 101 with a bogus accept value and asserts onOpen never fires. --- .../netty/handler/WebSocketHandler.java | 1 + .../ws/WebSocketHandshakeValidationTest.java | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java index 533322f4bd..18e8a3d073 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java @@ -67,6 +67,7 @@ private void upgrade(Channel channel, NettyResponseFuture future, WebSocketUp String key = getAcceptKey(future.getNettyRequest().getHttpRequest().headers().get(SEC_WEBSOCKET_KEY)); if (accept == null || !accept.equals(key)) { requestSender.abort(channel, future, new IOException("Invalid challenge. Actual: " + accept + ". Expected: " + key)); + return; } // set back the future so the protocol gets notified of frames diff --git a/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java b/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java new file mode 100644 index 0000000000..45fe29c2c7 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.ws; + +import org.asynchttpclient.AsyncHttpClient; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import static org.asynchttpclient.Dsl.asyncHttpClient; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.fail; + +/** + * RFC 6455 section 4.1: if the {@code Sec-WebSocket-Accept} value in the server's opening handshake does + * not match the expected base64 of SHA-1 of the nonce, the client MUST fail the WebSocket connection. It + * must not go on to upgrade the pipeline or notify the listener of an open socket. + */ +public class WebSocketHandshakeValidationTest { + + private ServerSocket serverSocket; + private Thread serverThread; + + @AfterMethod + public void tearDown() throws Exception { + if (serverSocket != null) { + serverSocket.close(); + } + if (serverThread != null) { + serverThread.interrupt(); + } + } + + // Completes the HTTP upgrade with a 101 but a Sec-WebSocket-Accept that does not match the key. + private int startServerWithBadAccept() throws IOException { + serverSocket = new ServerSocket(0, 1, InetAddress.getByName("localhost")); + int port = serverSocket.getLocalPort(); + serverThread = new Thread(() -> { + try (Socket socket = serverSocket.accept()) { + InputStream in = socket.getInputStream(); + int b3 = -1, b2 = -1, b1 = -1, b; + while ((b = in.read()) != -1) { + if (b3 == '\r' && b2 == '\n' && b1 == '\r' && b == '\n') { + break; + } + b3 = b2; + b2 = b1; + b1 = b; + } + OutputStream out = socket.getOutputStream(); + out.write(("HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: this-is-not-the-expected-accept\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII)); + out.flush(); + Thread.sleep(5000); + } catch (Exception ignored) { + } + }); + serverThread.setDaemon(true); + serverThread.start(); + return port; + } + + @Test(timeOut = 30000) + public void doesNotUpgradeOnInvalidAcceptKey() throws Exception { + int port = startServerWithBadAccept(); + CountDownLatch openLatch = new CountDownLatch(1); + + try (AsyncHttpClient c = asyncHttpClient()) { + try { + c.prepareGet("ws://localhost:" + port + "/") + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() { + @Override + public void onOpen(WebSocket websocket) { + openLatch.countDown(); + } + + @Override + public void onClose(WebSocket websocket, int code, String reason) { + } + + @Override + public void onError(Throwable t) { + } + }).build()).get(); + fail("the handshake must fail when Sec-WebSocket-Accept is invalid"); + } catch (ExecutionException expected) { + // the request is aborted with "Invalid challenge" + } + + assertFalse(openLatch.await(2, TimeUnit.SECONDS), + "onOpen must not fire when the server Sec-WebSocket-Accept is invalid"); + } + } +} From e1f5fc88fe211d3f64032c33b91093ba3d5e793d Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:25:02 +0000 Subject: [PATCH 13/32] Use SecureRandom for the Digest auth cnonce Motivation: Realm.Builder.newCnonce drew the client nonce from ThreadLocalRandom, a non-cryptographic PRNG whose output is predictable from observed values. RFC 7616 section 3.3 requires the cnonce to be unpredictable: it is what stops a hostile or compromised server from controlling the whole digest input and precomputing responses, and it is what makes the client's proof of the shared secret fresh. NtlmEngine in this same codebase already uses SecureRandom for its nonce. Modification: Draw the cnonce bytes from a SecureRandom held in a ThreadLocal on Realm.Builder, so the per-thread instance is reused rather than constructed per request. The generated value is still MD5-folded and hex-encoded exactly as before, so the cnonce keeps its length and shape and no wire format changes. Result: The Digest cnonce is now unpredictable. Added DigestCnonceTest, which asserts the generator is a SecureRandom and that each realm gets a fresh cnonce. --- .../main/java/org/asynchttpclient/Realm.java | 7 +- .../org/asynchttpclient/DigestCnonceTest.java | 77 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/DigestCnonceTest.java diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index c6324fd0b4..35cd8b4b0a 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -23,8 +23,8 @@ import java.nio.charset.Charset; import java.security.MessageDigest; +import java.security.SecureRandom; import java.util.Map; -import java.util.concurrent.ThreadLocalRandom; import static java.nio.charset.StandardCharsets.*; import static org.asynchttpclient.util.Assertions.assertNotNull; @@ -253,6 +253,9 @@ public enum AuthScheme { */ public static class Builder { + // cnonce must be unpredictable (RFC 7616 section 3.3), like the NTLM nonce + private static final ThreadLocal CNONCE_RANDOM = ThreadLocal.withInitial(SecureRandom::new); + private final String principal; private final String password; private AuthScheme scheme; @@ -449,7 +452,7 @@ public Builder parseProxyAuthenticateHeader(String headerLine) { private void newCnonce(MessageDigest md) { byte[] b = new byte[8]; - ThreadLocalRandom.current().nextBytes(b); + CNONCE_RANDOM.get().nextBytes(b); b = md.digest(b); cnonce = toHexString(b); } diff --git a/client/src/test/java/org/asynchttpclient/DigestCnonceTest.java b/client/src/test/java/org/asynchttpclient/DigestCnonceTest.java new file mode 100644 index 0000000000..3a7f81bfe0 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/DigestCnonceTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient; + +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.security.SecureRandom; +import java.util.HashSet; +import java.util.Set; + +import static org.asynchttpclient.Dsl.digestAuthRealm; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * RFC 7616 section 3.3 requires the client nonce to be unpredictable: it is what stops a hostile or + * compromised server from choosing the whole digest input and precomputing responses. A non-cryptographic + * PRNG such as {@link java.util.concurrent.ThreadLocalRandom} does not provide that, so the cnonce must be + * drawn from a {@link SecureRandom}. + */ +public class DigestCnonceTest { + + /** + * Structural check: whatever generator {@code Realm.Builder} holds for the cnonce, it must be a + * {@link SecureRandom}. Deliberately does not hard-code the field name so a rename does not break it. + */ + @Test + public void cnonceIsDrawnFromASecureRandom() throws Exception { + SecureRandom found = null; + for (Field field : Realm.Builder.class.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers())) { + continue; + } + field.setAccessible(true); + Object value = field.get(null); + if (value instanceof SecureRandom) { + found = (SecureRandom) value; + } else if (value instanceof ThreadLocal) { + Object supplied = ((ThreadLocal) value).get(); + if (supplied instanceof SecureRandom) { + found = (SecureRandom) supplied; + } + } + } + + assertNotNull(found, "Realm.Builder must draw the Digest cnonce from a SecureRandom (RFC 7616 section 3.3)"); + } + + @Test + public void everyDigestRealmGetsItsOwnCnonce() { + Set cnonces = new HashSet<>(); + for (int i = 0; i < 64; i++) { + Realm realm = digestAuthRealm("user", "password") + .setRealmName("realm") + .setNonce("aabbccddeeff") + .setQop("auth") + .build(); + assertNotNull(realm.getCnonce(), "a Digest realm built against a server nonce must carry a cnonce"); + cnonces.add(realm.getCnonce()); + } + + assertTrue(cnonces.size() == 64, "each Digest realm must get a fresh cnonce, got " + cnonces.size() + " distinct out of 64"); + } +} From 0422e11c954c4b026e986d1458f7a7a2eab7e591 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:28:27 +0000 Subject: [PATCH 14/32] Harden the resumable index store against local tampering Motivation: The resumable download index lives at a fixed, predictable path in the shared temp directory: /ahc/ResumableAsyncHandler.properties. It was created with mkdirs()/createNewFile(), i.e. with the process umask, so on a typical multi-user host the file listing every URL being downloaded was world-readable. Worse, save() opened it with Files.newOutputStream, which follows symlinks: another local user could plant a symlink at that path and have AHC truncate and overwrite the target on its behalf. load() followed the link too, so planted content was read back as resume offsets. Modification: - Create the directory and the store with owner-only POSIX permissions applied atomically at creation time via PosixFilePermissions.asFileAttribute, not chmod-after-create. - save() deletes any existing entry (the link itself, not its target) and opens with CREATE_NEW, so a re-plant that wins the race makes the open fail instead of writing through it. - The directory check uses NOFOLLOW_LINKS, so a symlinked "ahc" directory is refused rather than followed. - load() opens with NOFOLLOW_LINKS, so a planted symlink cannot feed attacker-chosen offsets back in. - The POSIX attributes are only applied when the file system reports a "posix" attribute view, so Windows keeps working unchanged. Result: The index is no longer readable by other local users and can no longer be used to make AHC write through or read from a planted symlink. Added PropertiesBasedResumableProcessorStoreTest; the existing PropertiesBasedResumableProcessorTest still passes. --- .../PropertiesBasedResumableProcessor.java | 49 +++++--- ...rtiesBasedResumableProcessorStoreTest.java | 105 ++++++++++++++++++ 2 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java b/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java index 1eb99f11ba..ea1f2066c6 100644 --- a/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java +++ b/client/src/main/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessor.java @@ -16,14 +16,25 @@ import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileNotFoundException; import java.io.OutputStream; +import java.nio.channels.Channels; +import java.nio.file.FileSystems; import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.EnumSet; import java.util.Map; import java.util.Scanner; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.CREATE_NEW; +import static java.nio.file.StandardOpenOption.WRITE; import static org.asynchttpclient.util.MiscUtils.closeSilently; /** @@ -34,8 +45,19 @@ public class PropertiesBasedResumableProcessor implements ResumableAsyncHandler. private final static Logger log = LoggerFactory.getLogger(PropertiesBasedResumableProcessor.class); private final static File TMP = new File(System.getProperty("java.io.tmpdir"), "ahc"); private final static String storeName = "ResumableAsyncHandler.properties"; + private final static boolean POSIX = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); + private final static FileAttribute[] DIR_ATTRIBUTES = ownerOnlyAttributes("rwx------"); + private final static FileAttribute[] FILE_ATTRIBUTES = ownerOnlyAttributes("rw-------"); + private final static Set CREATE_OPTIONS = EnumSet.of(WRITE, CREATE_NEW); + private final ConcurrentHashMap properties = new ConcurrentHashMap<>(); + private static FileAttribute[] ownerOnlyAttributes(String permissions) { + return POSIX + ? new FileAttribute[]{PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString(permissions))} + : new FileAttribute[0]; + } + private static String append(Map.Entry e) { return e.getKey() + '=' + e.getValue() + '\n'; } @@ -67,18 +89,18 @@ public void save(Map map) { OutputStream os = null; try { - if (!TMP.exists() && !TMP.mkdirs()) { - throw new IllegalStateException("Unable to create directory: " + TMP.getAbsolutePath()); - } - File f = new File(TMP, storeName); - if (!f.exists() && !f.createNewFile()) { - throw new IllegalStateException("Unable to create temp file: " + f.getAbsolutePath()); - } - if (!f.canWrite()) { - throw new IllegalStateException(); + Path dir = TMP.toPath(); + if (!Files.isDirectory(dir, LinkOption.NOFOLLOW_LINKS)) { + Files.createDirectory(dir, DIR_ATTRIBUTES); } - os = Files.newOutputStream(f.toPath()); + // The store sits at a fixed path in the shared temp directory and holds the URLs being downloaded, + // so it is recreated here with owner-only permissions instead of being written through whatever is + // already at that path. CREATE_NEW after the delete fails rather than opens if another local user + // re-plants a file or a symlink in between. + Path f = dir.resolve(storeName); + Files.deleteIfExists(f); + os = Channels.newOutputStream(Files.newByteChannel(f, CREATE_OPTIONS, FILE_ATTRIBUTES)); for (Map.Entry e : properties.entrySet()) { os.write(append(e).getBytes(UTF_8)); } @@ -97,7 +119,8 @@ public void save(Map map) { public Map load() { Scanner scan = null; try { - scan = new Scanner(new File(TMP, storeName), UTF_8.name()); + // NOFOLLOW_LINKS: refuse to read the state back through a symlink planted at the predictable path + scan = new Scanner(Files.newInputStream(new File(TMP, storeName).toPath(), LinkOption.NOFOLLOW_LINKS), UTF_8.name()); scan.useDelimiter("[=\n]"); String key; @@ -108,7 +131,7 @@ public Map load() { properties.put(key, Long.valueOf(value)); } log.debug("Loading previous download state {}", properties.toString()); - } catch (FileNotFoundException ex) { + } catch (NoSuchFileException ex) { log.debug("Missing {}", storeName); } catch (Throwable ex) { // Survive any exceptions diff --git a/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java b/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java new file mode 100644 index 0000000000..4b907fcddf --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorStoreTest.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.handler.resumable; + +import org.testng.SkipException; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Covers how the resumable index store is created in the shared temp directory. The store sits at a fixed, + * predictable path that every local user can write to, so it must be created owner-only and must never be + * written or read through something another user planted there. + */ +public class PropertiesBasedResumableProcessorStoreTest { + + private static final Path STORE = Paths.get(System.getProperty("java.io.tmpdir"), "ahc", "ResumableAsyncHandler.properties"); + + private static void requirePosix() { + if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { + throw new SkipException("POSIX file permissions are not supported on this file system"); + } + } + + private static void deleteStore() throws IOException { + Files.deleteIfExists(STORE); + } + + @Test + public void storeIsCreatedOwnerOnly() throws IOException { + requirePosix(); + deleteStore(); + + PropertiesBasedResumableProcessor processor = new PropertiesBasedResumableProcessor(); + processor.put("http://localhost/owner-only.url", 15L); + processor.save(null); + + assertEquals(PosixFilePermissions.toString(Files.getPosixFilePermissions(STORE)), "rw-------", + "the store must be created owner-readable/writable only"); + } + + @Test + public void saveDoesNotWriteThroughASymlink() throws IOException { + requirePosix(); + deleteStore(); + Files.createDirectories(STORE.getParent()); + + Path target = Files.createTempFile("ahc-symlink-target", ".txt"); + try { + Files.write(target, "untouched".getBytes(StandardCharsets.UTF_8)); + Files.createSymbolicLink(STORE, target); + + PropertiesBasedResumableProcessor processor = new PropertiesBasedResumableProcessor(); + processor.put("http://localhost/symlink.url", 15L); + processor.save(null); + + assertEquals(new String(Files.readAllBytes(target), StandardCharsets.UTF_8), "untouched", + "a symlink planted at the store path must not be followed and its target must not be truncated"); + } finally { + Files.deleteIfExists(target); + deleteStore(); + } + } + + @Test + public void loadDoesNotReadThroughASymlink() throws IOException { + requirePosix(); + deleteStore(); + Files.createDirectories(STORE.getParent()); + + Path target = Files.createTempFile("ahc-symlink-source", ".txt"); + try { + Files.write(target, "http://localhost/planted.url=42\n".getBytes(StandardCharsets.UTF_8)); + Files.createSymbolicLink(STORE, target); + + Map loaded = new PropertiesBasedResumableProcessor().load(); + + assertTrue(loaded.isEmpty(), "state must not be read back through a planted symlink, got " + loaded); + } finally { + Files.deleteIfExists(target); + deleteStore(); + } + } +} From e9f2f7423e0f6503f529656f2955a1317e56ba14 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Wed, 5 Aug 2026 21:32:27 +0000 Subject: [PATCH 15/32] Bound HTTP/1.1 response decompression to guard against decompression bombs Motivation: Both HttpContentDecompressor construction sites in ChannelManager used the no-arg constructor, which means maxAllocation=0, i.e. unbounded. A server (or anything that can inject a response) could return a few KiB of gzip that inflates to gigabytes, and AHC would allocate all of it before the caller ever saw a byte: a remote out-of-memory with a trivially small request. Modification: Add a maxDecompressedResponseSize config knob, following the existing convention for every other knob (constant and default accessor in AsyncHttpClientConfigDefaults, entry in ahc-default.properties, field/getter/Builder setter on DefaultAsyncHttpClientConfig, override in the typesafe-config extra). It is declared on AsyncHttpClientConfig as a default method, like isStripAuthorizationOnRedirect, so a third-party implementation of that interface still compiles and gets the safe default. The value defaults to 256 MiB and is passed to both decompressor construction sites, including the keepEncodingHeader anonymous subclass which now forwards it to super. Any non-positive value maps to Netty's 0, which keeps the old unbounded behaviour for callers who deliberately want it. Result: A compressed response that inflates past the ceiling now fails the request with a Netty DecompressionException instead of exhausting the heap. Added Http1DecompressionLimitTest covering over-limit, under-limit and limit-disabled, plus a default-value case in AsyncHttpClientDefaultsTest. --- .../AsyncHttpClientConfig.java | 13 ++ .../DefaultAsyncHttpClientConfig.java | 22 +++ .../config/AsyncHttpClientConfigDefaults.java | 5 + .../netty/channel/ChannelManager.java | 17 +- .../config/ahc-default.properties | 1 + .../AsyncHttpClientDefaultsTest.java | 5 + .../Http1DecompressionLimitTest.java | 159 ++++++++++++++++++ .../AsyncHttpClientTypesafeConfig.java | 5 + 8 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index d54f6d1698..7711337510 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -22,6 +22,7 @@ import io.netty.util.Timer; import org.asynchttpclient.channel.ChannelPool; import org.asynchttpclient.channel.KeepAliveStrategy; +import org.asynchttpclient.config.AsyncHttpClientConfigDefaults; import org.asynchttpclient.cookie.CookieStore; import org.asynchttpclient.filter.IOExceptionFilter; import org.asynchttpclient.filter.RequestFilter; @@ -291,6 +292,7 @@ public interface AsyncHttpClientConfig { boolean isKeepEncodingHeader(); + int getShutdownQuietPeriod(); int getShutdownTimeout(); @@ -356,6 +358,17 @@ default boolean isStripAuthorizationOnRedirect() { return false; } + /** + * The maximum number of bytes a compressed response body may inflate to. Guards against a + * "decompression bomb": a small response that expands without bound once decoded. {@code 0} disables the + * limit and restores Netty's unbounded behaviour. + * + * @return the decompressed response ceiling in bytes + */ + default int getMaxDecompressedResponseSize() { + return AsyncHttpClientConfigDefaults.defaultMaxDecompressedResponseSize(); + } + enum ResponseBodyPartFactory { EAGER { diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index dd1e3a9019..011402a275 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -61,6 +61,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final boolean useLaxCookieEncoder; private final boolean disableZeroCopy; private final boolean keepEncodingHeader; + private final int maxDecompressedResponseSize; private final ProxyServerSelector proxyServerSelector; private final boolean validateResponseHeaders; private final boolean stripAuthorizationOnRedirect; @@ -150,6 +151,7 @@ private DefaultAsyncHttpClientConfig(// http boolean useLaxCookieEncoder, boolean disableZeroCopy, boolean keepEncodingHeader, + int maxDecompressedResponseSize, ProxyServerSelector proxyServerSelector, boolean validateResponseHeaders, boolean aggregateWebSocketFrameFragments, @@ -239,6 +241,7 @@ private DefaultAsyncHttpClientConfig(// http this.useLaxCookieEncoder = useLaxCookieEncoder; this.disableZeroCopy = disableZeroCopy; this.keepEncodingHeader = keepEncodingHeader; + this.maxDecompressedResponseSize = maxDecompressedResponseSize; this.proxyServerSelector = proxyServerSelector; this.validateResponseHeaders = validateResponseHeaders; this.stripAuthorizationOnRedirect = stripAuthorizationOnRedirect; @@ -380,6 +383,11 @@ public boolean isKeepEncodingHeader() { return keepEncodingHeader; } + @Override + public int getMaxDecompressedResponseSize() { + return maxDecompressedResponseSize; + } + @Override public ProxyServerSelector getProxyServerSelector() { return proxyServerSelector; @@ -717,6 +725,7 @@ public static class Builder { private boolean useLaxCookieEncoder = defaultUseLaxCookieEncoder(); private boolean disableZeroCopy = defaultDisableZeroCopy(); private boolean keepEncodingHeader = defaultKeepEncodingHeader(); + private int maxDecompressedResponseSize = defaultMaxDecompressedResponseSize(); private ProxyServerSelector proxyServerSelector; private boolean useProxySelector = defaultUseProxySelector(); private boolean useProxyProperties = defaultUseProxyProperties(); @@ -809,6 +818,7 @@ public Builder(AsyncHttpClientConfig config) { useLaxCookieEncoder = config.isUseLaxCookieEncoder(); disableZeroCopy = config.isDisableZeroCopy(); keepEncodingHeader = config.isKeepEncodingHeader(); + maxDecompressedResponseSize = config.getMaxDecompressedResponseSize(); proxyServerSelector = config.getProxyServerSelector(); stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect(); @@ -940,6 +950,17 @@ public Builder setKeepEncodingHeader(boolean keepEncodingHeader) { return this; } + /** + * Bound how far a compressed response body may inflate, in bytes. {@code 0} disables the limit. + * + * @param maxDecompressedResponseSize the decompressed response ceiling in bytes + * @return this + */ + public Builder setMaxDecompressedResponseSize(int maxDecompressedResponseSize) { + this.maxDecompressedResponseSize = maxDecompressedResponseSize; + return this; + } + public Builder setProxyServerSelector(ProxyServerSelector proxyServerSelector) { this.proxyServerSelector = proxyServerSelector; return this; @@ -1325,6 +1346,7 @@ public DefaultAsyncHttpClientConfig build() { useLaxCookieEncoder, disableZeroCopy, keepEncodingHeader, + maxDecompressedResponseSize, resolveProxyServerSelector(), validateResponseHeaders, aggregateWebSocketFrameFragments, diff --git a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java index 14dcec3bfd..daaa1e83b7 100644 --- a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java +++ b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java @@ -67,6 +67,7 @@ public final class AsyncHttpClientConfigDefaults { public static final String WEBSOCKET_MAX_BUFFER_SIZE_CONFIG = "webSocketMaxBufferSize"; public static final String WEBSOCKET_MAX_FRAME_SIZE_CONFIG = "webSocketMaxFrameSize"; public static final String KEEP_ENCODING_HEADER_CONFIG = "keepEncodingHeader"; + public static final String MAX_DECOMPRESSED_RESPONSE_SIZE_CONFIG = "maxDecompressedResponseSize"; public static final String SHUTDOWN_QUIET_PERIOD_CONFIG = "shutdownQuietPeriod"; public static final String SHUTDOWN_TIMEOUT_CONFIG = "shutdownTimeout"; public static final String USE_NATIVE_TRANSPORT_CONFIG = "useNativeTransport"; @@ -282,6 +283,10 @@ public static boolean defaultKeepEncodingHeader() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + KEEP_ENCODING_HEADER_CONFIG); } + public static int defaultMaxDecompressedResponseSize() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + MAX_DECOMPRESSED_RESPONSE_SIZE_CONFIG); + } + public static int defaultShutdownQuietPeriod() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + SHUTDOWN_QUIET_PERIOD_CONFIG); } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index b93dfb380e..0abd05ec8f 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -254,15 +254,28 @@ protected void initChannel(Channel ch) { } private HttpContentDecompressor newHttpContentDecompressor() { + // Bound the decompressor's cumulative allocation to guard against decompression bombs (a small + // compressed body that inflates without limit). The no-arg constructor uses maxAllocation=0, i.e. + // unbounded; Netty throws a DecompressionException once the inflated size would exceed the ceiling. + int maxAllocation = maxDecompressedResponseSize(); if (config.isKeepEncodingHeader()) - return new HttpContentDecompressor() { + return new HttpContentDecompressor(maxAllocation) { @Override protected String getTargetContentEncoding(String contentEncoding) { return contentEncoding; } }; else - return new HttpContentDecompressor(); + return new HttpContentDecompressor(maxAllocation); + } + + /** + * The configured decompressed-response ceiling, normalised for Netty's maxAllocation parameter: any + * non-positive value means "no limit", which is what {@code 0} tells Netty. + */ + private int maxDecompressedResponseSize() { + int configured = config.getMaxDecompressedResponseSize(); + return configured > 0 ? configured : 0; } public final void tryToOfferChannelToPool(Channel channel, AsyncHandler asyncHandler, boolean keepAlive, Object partitionKey) { diff --git a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties index 62bc177726..014dc5d613 100644 --- a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties +++ b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties @@ -46,6 +46,7 @@ org.asynchttpclient.chunkedFileChunkSize=8192 org.asynchttpclient.webSocketMaxBufferSize=128000000 org.asynchttpclient.webSocketMaxFrameSize=10240 org.asynchttpclient.keepEncodingHeader=false +org.asynchttpclient.maxDecompressedResponseSize=268435456 org.asynchttpclient.shutdownQuietPeriod=2000 org.asynchttpclient.shutdownTimeout=15000 org.asynchttpclient.useNativeTransport=false diff --git a/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java b/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java index 8b7d172a45..6dcd27ad39 100644 --- a/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java +++ b/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java @@ -125,6 +125,11 @@ public void testDefaultHashedWheelTimerSize() { testIntegerSystemProperty("hashedWheelTimerSize", "defaultHashedWheelTimerSize", "512"); } + public void testDefaultMaxDecompressedResponseSize() { + Assert.assertEquals(AsyncHttpClientConfigDefaults.defaultMaxDecompressedResponseSize(), 256 * 1024 * 1024); + testIntegerSystemProperty("maxDecompressedResponseSize", "defaultMaxDecompressedResponseSize", "1024"); + } + private void testIntegerSystemProperty(String propertyName, String methodName, String value) { String previous = System.getProperty(ASYNC_CLIENT_CONFIG_ROOT + propertyName); System.setProperty(ASYNC_CLIENT_CONFIG_ROOT + propertyName, value); diff --git a/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java b/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java new file mode 100644 index 0000000000..a46c92f71c --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; +import io.netty.handler.codec.compression.DecompressionException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPOutputStream; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +/** + * Verifies that HTTP/1.1 automatic response decompression honours the configured decompressed-size ceiling + * ({@link AsyncHttpClientConfig#getMaxDecompressedResponseSize()}), so a highly compressible body cannot + * inflate without bound (a "decompression bomb"). Before the fix both {@code HttpContentDecompressor} + * construction sites used the no-arg constructor (maxAllocation=0 = unbounded). See + * {@code ChannelManager#newHttpContentDecompressor}. + */ +public class Http1DecompressionLimitTest { + + // 4 MiB of a single repeated byte: gzips to a few KiB but inflates to 4 MiB. + private static final byte[] LARGE_PAYLOAD = new byte[4 * 1024 * 1024]; + + static { + Arrays.fill(LARGE_PAYLOAD, (byte) 'a'); + } + + private static HttpServer httpServer; + + @BeforeClass + public static void setupServer() throws Exception { + httpServer = HttpServer.create(new InetSocketAddress("localhost", 0), 0); + httpServer.createContext("/gzip-bomb").setHandler(new HttpHandler() { + @Override + public void handle(HttpExchange exchange) throws IOException { + exchange.getResponseHeaders().set("Content-Encoding", "gzip"); + exchange.sendResponseHeaders(200, 0); + OutputStream out = exchange.getResponseBody(); + try { + GZIPOutputStream gzip = new GZIPOutputStream(out); + gzip.write(LARGE_PAYLOAD); + gzip.finish(); + gzip.close(); + } finally { + out.close(); + } + } + }); + httpServer.start(); + } + + @AfterClass + public static void stopServer() { + if (httpServer != null) { + httpServer.stop(0); + } + } + + private static AsyncHttpClient clientWithLimit(int maxDecompressedResponseSize) { + return clientWithLimit(maxDecompressedResponseSize, false); + } + + private static AsyncHttpClient clientWithLimit(int maxDecompressedResponseSize, boolean keepEncodingHeader) { + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setCompressionEnforced(true) + .setKeepEncodingHeader(keepEncodingHeader) + .setMaxDecompressedResponseSize(maxDecompressedResponseSize) + .build(); + return new DefaultAsyncHttpClient(config); + } + + private static String url() { + return "http://localhost:" + httpServer.getAddress().getPort() + "/gzip-bomb"; + } + + private static boolean hasDecompressionCause(Throwable t) { + for (Throwable c = t; c != null; c = c.getCause()) { + if (c instanceof DecompressionException) { + return true; + } + } + return false; + } + + @Test + public void decompressionBeyondLimitFails() throws Exception { + // 256 KiB ceiling, but the body inflates to 4 MiB -> Netty must abort with a DecompressionException + // rather than allocating the full 4 MiB. + try (AsyncHttpClient client = clientWithLimit(256 * 1024)) { + try { + client.prepareGet(url()).execute().get(30, TimeUnit.SECONDS); + fail("a body inflating past the configured ceiling must not be delivered"); + } catch (ExecutionException ex) { + assertTrue(hasDecompressionCause(ex), + "expected a Netty DecompressionException in the cause chain but got: " + ex.getCause()); + } + } + } + + @Test + public void decompressionWithinLimitSucceeds() throws Exception { + // A generous ceiling comfortably above the 4 MiB inflated size lets the same body through unchanged. + try (AsyncHttpClient client = clientWithLimit(64 * 1024 * 1024)) { + Response response = client.prepareGet(url()).execute().get(30, TimeUnit.SECONDS); + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length); + } + } + + @Test + public void decompressionBeyondLimitFailsWithKeepEncodingHeader() throws Exception { + // keepEncodingHeader selects the anonymous HttpContentDecompressor subclass, a separate construction + // site that has to forward the ceiling to super. Covers the half-fix where only the plain branch is + // bounded. + try (AsyncHttpClient client = clientWithLimit(256 * 1024, true)) { + try { + client.prepareGet(url()).execute().get(30, TimeUnit.SECONDS); + fail("the keepEncodingHeader decompressor must honour the ceiling too"); + } catch (ExecutionException ex) { + assertTrue(hasDecompressionCause(ex), + "expected a Netty DecompressionException in the cause chain but got: " + ex.getCause()); + } + } + } + + @Test + public void zeroDisablesTheLimit() throws Exception { + // 0 keeps Netty's unbounded behaviour for callers that deliberately opt out. + try (AsyncHttpClient client = clientWithLimit(0)) { + Response response = client.prepareGet(url()).execute().get(30, TimeUnit.SECONDS); + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length); + } + } +} diff --git a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java index 0092eb2a73..b0dbbddddd 100644 --- a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java +++ b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java @@ -289,6 +289,11 @@ public boolean isKeepEncodingHeader() { return getBooleanOpt(KEEP_ENCODING_HEADER_CONFIG).orElse(defaultKeepEncodingHeader()); } + @Override + public int getMaxDecompressedResponseSize() { + return getIntegerOpt(MAX_DECOMPRESSED_RESPONSE_SIZE_CONFIG).orElse(defaultMaxDecompressedResponseSize()); + } + @Override public int getShutdownQuietPeriod() { return getIntegerOpt(SHUTDOWN_QUIET_PERIOD_CONFIG).orElse(defaultShutdownQuietPeriod()); From bc77a1bf9cbc27a21e42748ca11e4c126eef2741 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 6 Aug 2026 21:26:49 +0000 Subject: [PATCH 16/32] Strip userinfo from the Digest uri= parameter Motivation: AuthenticatorUtils.computeRealmURI renders the absolute-form request target with Uri.toUrl(), which emits the deprecated userinfo subcomponent. For a request built from a URL like http://user:secret@host/path that puts "user:secret@" inside the Digest uri="..." parameter, so the password travels in the clear on exactly the hop Digest exists to avoid sending it on, and it is also covered by the digest so it cannot be scrubbed downstream. RFC 9110 section 4.2.4 says a sender MUST NOT generate the userinfo subcomponent in a request target. Both arms of the useAbsoluteURI branch were affected: the omitQuery arm rebuilds the Uri via withNewQuery(null), which carries the userinfo over. Modification: Use Uri.toUrlWithoutUserInfo() in both arms. The relative branch already emitted path and query only, so it is unchanged. Result: The Digest uri= parameter no longer carries credentials. Added AuthenticatorUtilsTest covering both absolute arms, the relative branch, a userinfo-free URI (unchanged output), and the emitted Authorization header end to end. --- .../util/AuthenticatorUtils.java | 8 +- .../util/AuthenticatorUtilsTest.java | 87 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 00d69af7d2..77cbcee938 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -55,7 +55,13 @@ private static String computeBasicAuthentication(String principal, String passwo public static String computeRealmURI(Uri uri, boolean useAbsoluteURI, boolean omitQuery) { if (useAbsoluteURI) { - return omitQuery && MiscUtils.isNonEmpty(uri.getQuery()) ? uri.withNewQuery(null).toUrl() : uri.toUrl(); + // Absolute form, so this string goes on the wire as the Digest uri="..." parameter, and the digest + // itself is computed over it. RFC 9110 section 4.2.4 forbids generating the userinfo subcomponent in + // a request target, and Digest is used precisely on hops where the password must not travel in the + // clear - toUrl() would render "user:secret@" straight into the header. Use the userinfo-free form. + return omitQuery && MiscUtils.isNonEmpty(uri.getQuery()) + ? uri.withNewQuery(null).toUrlWithoutUserInfo() + : uri.toUrlWithoutUserInfo(); } else { String path = uri.getNonEmptyPath(); return omitQuery || !MiscUtils.isNonEmpty(uri.getQuery()) ? path : path + "?" + uri.getQuery(); diff --git a/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java new file mode 100644 index 0000000000..7d67ac2348 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/util/AuthenticatorUtilsTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.util; + +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.uri.Uri; +import org.testng.annotations.Test; + +import static org.asynchttpclient.util.AuthenticatorUtils.computeRealmURI; +import static org.asynchttpclient.util.AuthenticatorUtils.perRequestAuthorizationHeader; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class AuthenticatorUtilsTest { + + private static final Uri URI_WITH_USERINFO = Uri.create("http://user:secret@example.com:8080/path?q=1"); + + /** + * The Digest {@code uri=} parameter goes on the wire and is covered by the digest itself. RFC 9110 + * section 4.2.4 forbids generating the userinfo subcomponent in a request target, and rendering it here + * would put the password in the clear on the very hop Digest exists to protect. + */ + @Test + public void computeRealmURIAbsoluteDropsUserInfo() { + assertEquals(computeRealmURI(URI_WITH_USERINFO, true, false), "http://example.com:8080/path?q=1"); + } + + @Test + public void computeRealmURIAbsoluteOmitQueryDropsUserInfo() { + // omitQuery rebuilds the Uri via withNewQuery(null), which carries the userinfo over: a second, + // independent path to the same leak. + assertEquals(computeRealmURI(URI_WITH_USERINFO, true, true), "http://example.com:8080/path"); + } + + @Test + public void computeRealmURIAbsoluteWithoutUserInfoIsUnchanged() { + Uri uri = Uri.create("http://example.com:8080/path?q=1"); + assertEquals(computeRealmURI(uri, true, false), "http://example.com:8080/path?q=1"); + assertEquals(computeRealmURI(uri, true, true), "http://example.com:8080/path"); + } + + @Test + public void computeRealmURIRelativeIsUnchanged() { + assertEquals(computeRealmURI(URI_WITH_USERINFO, false, false), "/path?q=1"); + assertEquals(computeRealmURI(URI_WITH_USERINFO, false, true), "/path"); + } + + /** + * End-to-end over the header the client actually emits: no fragment of the userinfo may appear anywhere + * in the Authorization value. + */ + @Test + public void digestAuthorizationHeaderDoesNotCarryUserInfo() { + Realm realm = new Realm.Builder("user", "secret") + .setScheme(Realm.AuthScheme.DIGEST) + .setUri(URI_WITH_USERINFO) + .setMethodName("GET") + .setUsePreemptiveAuth(true) + .setUseAbsoluteURI(true) + .setRealmName("realm") + .setNonce("nonce") + .build(); + + Request request = new RequestBuilder("GET").setUri(URI_WITH_USERINFO).build(); + String header = perRequestAuthorizationHeader(request, realm); + + assertTrue(header.startsWith("Digest "), "expected a Digest header but got: " + header); + assertTrue(header.contains("uri=\"http://example.com:8080/path?q=1\""), + "expected a userinfo-free uri parameter but got: " + header); + assertFalse(header.contains("secret@"), "password must not appear in the Digest header: " + header); + assertFalse(header.contains("user:secret"), "credentials must not appear in the Digest header: " + header); + } +} From c37d12557d1b00a28664b7fb7f7fb7e5214e74b0 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 6 Aug 2026 21:30:37 +0000 Subject: [PATCH 17/32] Use SecureRandom for the multipart boundary and the WebSocket key Motivation: Two generators that must be unpredictable were still drawing from a non-cryptographic source. HttpUtils.computeMultipartBoundary used the JDK's ThreadLocalRandom, documented as not cryptographically secure. Part content is never escaped, so the boundary is the only separator: a caller that can predict it can close a part early and append a forged Content-Disposition that the receiving server parses as an extra form field. It is also the widest window a peer ever gets onto that generator - 30 to 40 consecutive nextInt(64) draws echoed verbatim in the Content-Type header and again in the body - which over-determines the state and makes every later draw from the same thread predictable, including the Digest cnonce that was just moved to SecureRandom. WebSocketUtils.getWebSocketKey built the 16-byte Sec-WebSocket-Key from Netty's ThreadLocalRandom, a 48-bit LCG. Sec-WebSocket-Accept is a pure function of that key, so a party who can guess it can precompute a valid accept value and the handshake check no longer proves the 101 came from a peer that saw the request. RFC 6455 section 10.3 requires a strong source of entropy here. Modification: Both now draw from a ThreadLocal, the same idiom already used for the Digest cnonce in Realm. getWebSocketKey fills the nonce with a single nextBytes call instead of sixteen nextInt(256) calls. OAuthSignatureCalculatorInstance is deliberately left alone: RFC 5849 requires its nonce to be unique, not unpredictable. Result: Neither value can be derived from prior observed output. Added coverage for the generator source and for the value contracts (boundary length and alphabet, 16-byte key, RFC 6455 section 1.3 accept-key vector). --- .../org/asynchttpclient/util/HttpUtils.java | 11 +++- .../asynchttpclient/ws/WebSocketUtils.java | 13 ++-- .../asynchttpclient/util/HttpUtilsTest.java | 36 +++++++++++ .../ws/WebSocketUtilsTest.java | 61 +++++++++++++++++++ 4 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/ws/WebSocketUtilsTest.java diff --git a/client/src/main/java/org/asynchttpclient/util/HttpUtils.java b/client/src/main/java/org/asynchttpclient/util/HttpUtils.java index 779dba9c78..5930d31d7e 100644 --- a/client/src/main/java/org/asynchttpclient/util/HttpUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/HttpUtils.java @@ -23,8 +23,8 @@ import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.security.SecureRandom; import java.util.List; -import java.util.concurrent.ThreadLocalRandom; import static java.nio.charset.StandardCharsets.*; @@ -114,9 +114,16 @@ private static String extractContentTypeAttribute(String contentType, String att // The pool of ASCII chars to be used for generating a multipart boundary. private static byte[] MULTIPART_CHARS = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes(US_ASCII); + // The boundary is what separates parts whose content is not escaped, so it must be unpredictable, like + // the cnonce in Realm. It is also the single largest window a peer gets onto the generator: 30 to 40 + // consecutive nextInt(64) outputs, echoed verbatim in the Content-Type header and in the body. With a + // non-cryptographic generator that over-determines the state, so a boundary observed on one request + // predicts every subsequent draw from the same thread - including the Digest cnonce. + private static final ThreadLocal BOUNDARY_RANDOM = ThreadLocal.withInitial(SecureRandom::new); + // a random size from 30 to 40 public static byte[] computeMultipartBoundary() { - ThreadLocalRandom random = ThreadLocalRandom.current(); + SecureRandom random = BOUNDARY_RANDOM.get(); byte[] bytes = new byte[random.nextInt(11) + 30]; for (int i = 0; i < bytes.length; i++) { bytes[i] = MULTIPART_CHARS[random.nextInt(MULTIPART_CHARS.length)]; diff --git a/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java b/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java index 31bd8f5c2a..4861ac7b92 100644 --- a/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java +++ b/client/src/main/java/org/asynchttpclient/ws/WebSocketUtils.java @@ -13,8 +13,7 @@ */ package org.asynchttpclient.ws; -import io.netty.util.internal.ThreadLocalRandom; - +import java.security.SecureRandom; import java.util.Base64; import static java.nio.charset.StandardCharsets.US_ASCII; @@ -23,12 +22,14 @@ public final class WebSocketUtils { private static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + // RFC 6455 section 10.3 requires the handshake nonce to come from a strong source of entropy: it is + // what proves the 101 was produced by a peer that saw this request. Netty's ThreadLocalRandom is a + // 48-bit LCG, so an off-path party who can guess the key can precompute a valid Sec-WebSocket-Accept. + private static final ThreadLocal KEY_RANDOM = ThreadLocal.withInitial(SecureRandom::new); + public static String getWebSocketKey() { byte[] nonce = new byte[16]; - ThreadLocalRandom random = ThreadLocalRandom.current(); - for (int i = 0; i < nonce.length; i++) { - nonce[i] = (byte) random.nextInt(256); - } + KEY_RANDOM.get().nextBytes(nonce); return Base64.getEncoder().encodeToString(nonce); } diff --git a/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java b/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java index aa9235101c..9bc71d0e15 100644 --- a/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java +++ b/client/src/test/java/org/asynchttpclient/util/HttpUtilsTest.java @@ -23,11 +23,16 @@ import org.asynchttpclient.uri.Uri; import org.testng.annotations.Test; +import java.lang.reflect.Field; import java.net.URLEncoder; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.security.SecureRandom; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Random; +import java.util.Set; import static io.netty.handler.codec.http.HttpHeaderValues.APPLICATION_JSON; import static java.nio.charset.StandardCharsets.*; @@ -113,6 +118,37 @@ public void testGetFollowRedirectPriorityGivenToRequest() { assertFalse(followRedirect, "Follow redirect value set in request should be given priority"); } + /** + * The multipart boundary is the only thing separating parts whose content is never escaped, so a caller + * that can predict it can close a part early and append a forged Content-Disposition. It is also the + * widest window a peer gets onto the generator - 30 to 40 consecutive draws echoed in the clear - so + * anything else drawn from the same generator (the Digest cnonce) inherits the weakness. It must come + * from a cryptographically strong source, not from ThreadLocalRandom. + */ + @Test + public void multipartBoundaryComesFromASecureGenerator() throws Exception { + Field field = HttpUtils.class.getDeclaredField("BOUNDARY_RANDOM"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + ThreadLocal holder = (ThreadLocal) field.get(null); + assertTrue(holder.get() instanceof SecureRandom, + "the multipart boundary generator must be a SecureRandom but was: " + holder.get().getClass()); + } + + @Test + public void computeMultipartBoundaryRespectsItsContract() { + String allowed = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + Set boundaries = new HashSet<>(); + for (int i = 0; i < 1000; i++) { + String boundary = new String(HttpUtils.computeMultipartBoundary(), US_ASCII); + assertTrue(boundary.length() >= 30 && boundary.length() <= 40, "Unexpected boundary length: " + boundary.length()); + for (int j = 0; j < boundary.length(); j++) { + assertTrue(allowed.indexOf(boundary.charAt(j)) != -1, "Illegal boundary char: " + boundary.charAt(j)); + } + assertTrue(boundaries.add(boundary), "Boundary was generated twice: " + boundary); + } + } + private void formUrlEncoding(Charset charset) throws Exception { String key = "key"; String value = "中文"; diff --git a/client/src/test/java/org/asynchttpclient/ws/WebSocketUtilsTest.java b/client/src/test/java/org/asynchttpclient/ws/WebSocketUtilsTest.java new file mode 100644 index 0000000000..5dae180d7d --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/ws/WebSocketUtilsTest.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.ws; + +import org.testng.annotations.Test; + +import java.lang.reflect.Field; +import java.security.SecureRandom; +import java.util.Base64; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +public class WebSocketUtilsTest { + + /** + * RFC 6455 section 10.3: "the nonce MUST be selected randomly for each connection" from a source that + * "cannot be guessed", because Sec-WebSocket-Accept is a pure function of the key. An off-path party who + * can predict the key can precompute a valid accept value, so the handshake check stops proving the 101 + * came from a peer that saw the request. Netty's ThreadLocalRandom is a 48-bit LCG. + */ + @Test + public void webSocketKeyComesFromASecureGenerator() throws Exception { + Field field = WebSocketUtils.class.getDeclaredField("KEY_RANDOM"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + ThreadLocal holder = (ThreadLocal) field.get(null); + assertTrue(holder.get() instanceof SecureRandom, + "the Sec-WebSocket-Key generator must be a SecureRandom but was: " + holder.get().getClass()); + } + + @Test + public void webSocketKeyIsSixteenFreshBytes() { + Set keys = new HashSet<>(); + for (int i = 0; i < 1000; i++) { + String key = WebSocketUtils.getWebSocketKey(); + assertEquals(Base64.getDecoder().decode(key).length, 16, "RFC 6455 requires a 16-byte nonce: " + key); + assertTrue(keys.add(key), "Sec-WebSocket-Key was generated twice: " + key); + } + } + + @Test + public void acceptKeyMatchesTheRfcExample() { + // RFC 6455 section 1.3. + assertEquals(WebSocketUtils.getAcceptKey("dGhlIHNhbXBsZSBub25jZQ=="), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="); + } +} From f289ec22a2d7ad49945cd0fd5abe63ed73cc1975 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 6 Aug 2026 21:33:50 +0000 Subject: [PATCH 18/32] Bind the origin Kerberos/SPNEGO token to the origin, not the proxy Motivation: AuthenticatorUtils.perConnectionAuthorizationHeader minted the ORIGIN realm's Negotiate token against proxyServer.getHost() whenever a proxy was configured, ignoring the virtual host and the request URI. That produces a service ticket for the proxy's SPN from the origin's credential: a confused deputy, where the credential the caller intended for the origin is delivered to the proxy and is usable only by it, while origin authentication fails for want of a ticket it can accept. The proxy leg has never needed this - it has its own path in perConnectionProxyAuthorizationHeader and in ProxyUnauthorized407Interceptor. Modification: Always derive the target service from the origin: the virtual host when set, otherwise the request URI host. The proxyServer parameter is kept so the public signature is unchanged in a patch release. Backport of the 3.x fix in #2245. Result: The origin's Negotiate token targets the origin service whether or not a proxy is configured, and proxy authentication is untouched. Added PerConnectionAuthorizationHeaderTest, which asserts on the service principal name the engine derives (minting a real token needs a KDC, but the SPN is chosen before any GSS or network work) across proxy, virtual-host and no-proxy cases, plus a case pinning the proxy realm's own per-connection header. --- .../util/AuthenticatorUtils.java | 13 +- .../PerConnectionAuthorizationHeaderTest.java | 142 ++++++++++++++++++ 2 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 77cbcee938..757f87a508 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -172,13 +172,12 @@ public static String perConnectionAuthorizationHeader(Request request, ProxyServ break; case KERBEROS: case SPNEGO: - String host; - if (proxyServer != null) - host = proxyServer.getHost(); - else if (request.getVirtualHost() != null) - host = request.getVirtualHost(); - else - host = request.getUri().getHost(); + // The origin realm's Negotiate token must target the origin service even when a proxy is + // configured. Minting it against the proxy host produced a service ticket for the proxy's SPN: a + // confused deputy where the origin's credential is delivered to, and only usable by, the proxy, + // while origin authentication fails. The proxy realm has its own path, in + // perConnectionProxyAuthorizationHeader, and is unaffected. + String host = request.getVirtualHost() != null ? request.getVirtualHost() : request.getUri().getHost(); try { authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance( diff --git a/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java b/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java new file mode 100644 index 0000000000..3b68c83658 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.util; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AppenderBase; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.proxy.ProxyServer; +import org.asynchttpclient.spnego.SpnegoEngine; +import org.slf4j.LoggerFactory; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * The origin realm's Kerberos/SPNEGO token must be minted against the ORIGIN service, never against the + * proxy, even when a proxy is configured. Building it from {@code proxyServer.getHost()} produced a service + * ticket for the proxy's SPN: a confused deputy in which the origin's credential is delivered to, and only + * usable by, the proxy, while origin authentication fails. + * + *

Minting a real token needs a KDC, so the assertion is made on the service principal name the engine + * derives, which is logged before any GSS or network work happens. + */ +public class PerConnectionAuthorizationHeaderTest { + + private static final class CapturingAppender extends AppenderBase { + private final List messages = new CopyOnWriteArrayList<>(); + + @Override + protected void append(ILoggingEvent event) { + messages.add(event.getFormattedMessage()); + } + } + + /** + * Drives perConnectionAuthorizationHeader with a SPNEGO origin realm and returns the service principal + * name SpnegoEngine derived. Token generation itself fails without a KDC, which is irrelevant here: the + * SPN is chosen, and logged, before the first GSS call. + */ + private static String servicePrincipalNameFor(Request request, ProxyServer proxyServer) { + ch.qos.logback.classic.Logger engineLogger = + (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(SpnegoEngine.class); + Level previousLevel = engineLogger.getLevel(); + CapturingAppender appender = new CapturingAppender(); + appender.start(); + engineLogger.addAppender(appender); + engineLogger.setLevel(Level.DEBUG); + + try { + Realm realm = new Realm.Builder("principal", "password") + .setScheme(Realm.AuthScheme.SPNEGO) + .setUsePreemptiveAuth(true) + // Pin off DNS so the derived name is exactly the host we passed in. + .setUseCanonicalHostname(false) + .build(); + try { + AuthenticatorUtils.perConnectionAuthorizationHeader(request, proxyServer, realm); + } catch (Throwable expected) { + // No KDC in the build environment, so the handshake cannot complete. The SPN has already been + // derived and logged by this point. + } + + for (String message : appender.messages) { + if (message.startsWith("Service Principal Name is ")) { + return message.substring("Service Principal Name is ".length()); + } + } + throw new AssertionError("SpnegoEngine never derived a service principal name; captured: " + appender.messages); + + } finally { + engineLogger.detachAppender(appender); + engineLogger.setLevel(previousLevel); + appender.stop(); + } + } + + @Test + public void spnegoTokenTargetsTheOriginEvenBehindAProxy() { + Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); + ProxyServer proxyServer = new ProxyServer.Builder("proxy.example.com", 8080).build(); + + String spn = servicePrincipalNameFor(request, proxyServer); + + assertEquals(spn, "HTTP@origin.example.com", + "the origin realm's Negotiate token must target the origin service"); + assertFalse(spn.contains("proxy.example.com"), + "the origin credential must not be minted for the proxy's SPN: " + spn); + } + + @Test + public void spnegoTokenPrefersTheVirtualHostBehindAProxy() { + Request request = new RequestBuilder("GET") + .setUrl("http://origin.example.com/resource") + .setVirtualHost("vhost.example.com") + .build(); + ProxyServer proxyServer = new ProxyServer.Builder("proxy.example.com", 8080).build(); + + assertEquals(servicePrincipalNameFor(request, proxyServer), "HTTP@vhost.example.com"); + } + + @Test + public void spnegoTokenTargetsTheOriginWithoutAProxy() { + Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); + + assertEquals(servicePrincipalNameFor(request, null), "HTTP@origin.example.com"); + } + + /** + * The proxy realm keeps its own path and is unaffected: NTLM is minted per connection there, and the + * Kerberos/SPNEGO proxy token is built against the proxy host by ProxyUnauthorized407Interceptor. + */ + @Test + public void proxyRealmStillGetsItsOwnPerConnectionHeader() { + Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); + Realm proxyRealm = new Realm.Builder("principal", "password") + .setScheme(Realm.AuthScheme.NTLM) + .setUsePreemptiveAuth(true) + .build(); + + String header = AuthenticatorUtils.perConnectionProxyAuthorizationHeader(request, proxyRealm); + assertTrue(header != null && header.startsWith("NTLM "), "expected an NTLM type-1 message but got: " + header); + } +} From 54e828c6a476b3fddee8ee0bc78a571cb18a0aa9 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 6 Aug 2026 21:41:39 +0000 Subject: [PATCH 19/32] Drop a WebSocket upgrade response that lands on a finished future Motivation: WebSocketHandler.handleRead upgraded on any 101 without asking whether the exchange was still live. A future can be finished while its channel is still open and still carrying the future as its attribute: NettyResponseFuture.cancel() sets isCancelled before it marks the channel discarded, and NettyResponseFuture.abort() nulls the future's channel reference without touching the channel at all. An upgrade response arriving in that window ran the full upgrade: the pipeline was swapped, the future was published back onto the channel and onOpen fired on a listener that had already been given onThrowable. The caller is told the request failed and is then handed a live WebSocket, on a connection nobody owns. Modification: Return early, closing the channel, when the future is already done. Backport of the 3.x guard. It is deliberately scoped to the HttpResponse branch and NOT hoisted to the whole method the way HttpHandler's equivalent is: upgrade() ends with future.done(), so isDone() is true for the entire life of a healthy WebSocket and a whole-method guard would drop every frame of every established connection. Result: A 101 that loses the race against the end of the exchange is dropped and the orphaned channel is closed. Added WebSocketHandlerDoneFutureTest, which sets that state up exactly at the handler seam - it observes [onError, onOpen] without the guard - plus two cases pinning that a live future still upgrades and an established WebSocket still receives frames, so the guard cannot later be widened. Also added an end-to-end late-101 case to WebSocketHandshakeValidationTest. --- .../netty/handler/WebSocketHandler.java | 10 + .../WebSocketHandlerDoneFutureTest.java | 225 ++++++++++++++++++ .../ws/WebSocketHandshakeValidationTest.java | 96 ++++++++ 3 files changed, 331 insertions(+) create mode 100644 client/src/test/java/org/asynchttpclient/netty/handler/WebSocketHandlerDoneFutureTest.java diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java index 18e8a3d073..c36f314b44 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/WebSocketHandler.java @@ -100,6 +100,16 @@ private void abort(Channel channel, NettyResponseFuture future, WebSocketUpgr public void handleRead(Channel channel, NettyResponseFuture future, Object e) throws Exception { if (e instanceof HttpResponse) { + // Unlike HttpHandler, this check cannot guard the whole method: a successful upgrade completes the + // future (see upgrade() below) and the channel then keeps serving frames, so isDone() is true for + // all normal WebSocket traffic. It belongs on the upgrade response alone, where a 101 arriving just + // as a request timeout aborted the future would otherwise still run upgrade() and deliver onOpen - + // and the rest of the WebSocket lifecycle - after onThrowable had already fired. + if (future.isDone()) { + channelManager.closeChannel(channel); + return; + } + HttpResponse response = (HttpResponse) e; if (logger.isDebugEnabled()) { HttpRequest httpRequest = future.getNettyRequest().getHttpRequest(); diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/WebSocketHandlerDoneFutureTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/WebSocketHandlerDoneFutureTest.java new file mode 100644 index 0000000000..b7f4a3fcbe --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/handler/WebSocketHandlerDoneFutureTest.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.handler; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timer; +import org.asynchttpclient.AsyncHttpClientConfig; +import org.asynchttpclient.AsyncHttpClientState; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.channel.ChannelPoolPartitioning; +import org.asynchttpclient.netty.NettyResponseFuture; +import org.asynchttpclient.netty.channel.ChannelManager; +import org.asynchttpclient.netty.channel.Channels; +import org.asynchttpclient.netty.request.NettyRequest; +import org.asynchttpclient.netty.request.NettyRequestSender; +import org.asynchttpclient.netty.ws.NettyWebSocket; +import org.asynchttpclient.ws.WebSocket; +import org.asynchttpclient.ws.WebSocketListener; +import org.asynchttpclient.ws.WebSocketUpgradeHandler; +import org.asynchttpclient.ws.WebSocketUtils; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.lang.reflect.Constructor; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; +import static io.netty.handler.codec.http.HttpHeaderNames.SEC_WEBSOCKET_ACCEPT; +import static io.netty.handler.codec.http.HttpHeaderNames.SEC_WEBSOCKET_KEY; +import static io.netty.handler.codec.http.HttpHeaderNames.UPGRADE; +import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS; +import static org.asynchttpclient.ws.WebSocketUtils.getAcceptKey; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A WebSocket upgrade response that lands on a future which is already finished must be dropped. + * {@link NettyResponseFuture#abort} and {@link NettyResponseFuture#cancel} both mark the future done before + * (cancel) or without (a direct abort) marking the channel discarded, so the event loop can still find the + * future as the channel attribute and deliver the 101 into {@code handleRead}. Without a guard the handler + * upgrades the pipeline and fires {@code onOpen} on a listener that has already had {@code onThrowable}: + * the caller is told the request failed and then handed a live WebSocket. + * + *

The window is a race in production, so it is reproduced here at the handler seam, where the state can + * be set up exactly. The two upgrade-still-works cases are not decoration: the guard MUST stay scoped to + * the {@code HttpResponse} branch, because {@code upgrade()} ends with {@code future.done()} and a + * whole-method guard would therefore drop every frame of every healthy WebSocket. + */ +public class WebSocketHandlerDoneFutureTest { + + private Timer timer; + private ChannelManager channelManager; + private WebSocketHandler handler; + private EmbeddedChannel channel; + private String webSocketKey; + + private final List events = new CopyOnWriteArrayList<>(); + + private final WebSocketListener listener = new WebSocketListener() { + @Override + public void onOpen(WebSocket websocket) { + events.add("onOpen"); + } + + @Override + public void onClose(WebSocket websocket, int code, String reason) { + events.add("onClose"); + } + + @Override + public void onError(Throwable t) { + events.add("onError"); + } + + @Override + public void onTextFrame(String payload, boolean finalFragment, int rsv) { + events.add("onTextFrame:" + payload); + } + }; + + // NettyRequest and AsyncHttpClientState are package-private to their own packages; a WebSocketHandler + // cannot be exercised in isolation without them. + private static NettyRequest newNettyRequest(HttpRequest httpRequest) throws Exception { + Constructor constructor = NettyRequest.class.getDeclaredConstructor(HttpRequest.class, + Class.forName("org.asynchttpclient.netty.request.body.NettyBody")); + constructor.setAccessible(true); + return constructor.newInstance(httpRequest, null); + } + + private static AsyncHttpClientState newClientState() throws Exception { + Constructor constructor = AsyncHttpClientState.class.getDeclaredConstructor(AtomicBoolean.class); + constructor.setAccessible(true); + return constructor.newInstance(new AtomicBoolean(false)); + } + + @BeforeMethod + public void setUp() throws Exception { + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build(); + timer = new HashedWheelTimer(); + channelManager = new ChannelManager(config, timer); + NettyRequestSender requestSender = new NettyRequestSender(config, channelManager, timer, newClientState()); + handler = new WebSocketHandler(config, channelManager, requestSender); + + channel = new EmbeddedChannel(); + // upgradePipelineForWebSockets() inserts the WebSocket codecs relative to the HTTP codec. + channel.pipeline().addLast(ChannelManager.HTTP_CLIENT_CODEC, new HttpClientCodec()); + + webSocketKey = WebSocketUtils.getWebSocketKey(); + events.clear(); + } + + @AfterMethod + public void tearDown() { + if (channel != null) { + channel.finishAndReleaseAll(); + } + if (channelManager != null) { + channelManager.close(); + } + if (timer != null) { + timer.stop(); + } + } + + private NettyResponseFuture newFuture() throws Exception { + HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + httpRequest.headers().set(SEC_WEBSOCKET_KEY, webSocketKey); + + Request request = new RequestBuilder("GET").setUrl("ws://localhost:8080/").build(); + WebSocketUpgradeHandler upgradeHandler = new WebSocketUpgradeHandler.Builder() + .addWebSocketListener(listener) + .build(); + + NettyResponseFuture future = new NettyResponseFuture<>(request, + upgradeHandler, + newNettyRequest(httpRequest), + 0, + ChannelPoolPartitioning.PerHostChannelPoolPartitioning.INSTANCE, + null, + null); + Channels.setAttribute(channel, future); + return future; + } + + /** + * A textbook-valid 101: nothing but the state of the future may stop the upgrade. + */ + private HttpResponse newUpgradeResponse() { + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, SWITCHING_PROTOCOLS); + response.headers() + .set(UPGRADE, "websocket") + .set(CONNECTION, "Upgrade") + .set(SEC_WEBSOCKET_ACCEPT, getAcceptKey(webSocketKey)); + return response; + } + + @Test + public void doesNotUpgradeWhenTheFutureIsAlreadyDone() throws Exception { + NettyResponseFuture future = newFuture(); + + // What a request timeout leaves behind: the exchange is over and the listener has been told. + future.abort(new TimeoutException("Request timeout")); + assertTrue(future.isDone()); + assertEquals(events, java.util.Collections.singletonList("onError")); + + handler.handleRead(channel, future, newUpgradeResponse()); + + assertFalse(events.contains("onOpen"), + "onOpen must not fire after the future was aborted, but got: " + events); + assertFalse(channel.isOpen(), "the orphaned channel must be closed rather than left upgraded"); + } + + @Test + public void stillUpgradesWhenTheFutureIsLive() throws Exception { + NettyResponseFuture future = newFuture(); + + handler.handleRead(channel, future, newUpgradeResponse()); + + assertTrue(events.contains("onOpen"), "a valid 101 on a live future must still upgrade, got: " + events); + assertFalse(events.contains("onError"), "the successful upgrade must not report an error: " + events); + } + + /** + * upgrade() completes the future, so isDone() is true for the entire life of a healthy WebSocket. The + * guard must not reach the frame branch. + */ + @Test + public void stillDeliversFramesAfterTheUpgradeCompletedTheFuture() throws Exception { + NettyResponseFuture future = newFuture(); + handler.handleRead(channel, future, newUpgradeResponse()); + assertTrue(future.isDone(), "upgrade() is expected to complete the future"); + + handler.handleRead(channel, future, new TextWebSocketFrame("hello")); + + assertTrue(events.contains("onTextFrame:hello"), + "frames on an established WebSocket must still be delivered, got: " + events); + } +} diff --git a/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java b/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java index 45fe29c2c7..9525d90141 100644 --- a/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java +++ b/client/src/test/java/org/asynchttpclient/ws/WebSocketHandshakeValidationTest.java @@ -14,6 +14,8 @@ package org.asynchttpclient.ws; import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.AsyncHttpClientConfig; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; import org.testng.annotations.AfterMethod; import org.testng.annotations.Test; @@ -30,6 +32,7 @@ import static org.asynchttpclient.Dsl.asyncHttpClient; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; /** @@ -115,4 +118,97 @@ public void onError(Throwable t) { "onOpen must not fire when the server Sec-WebSocket-Accept is invalid"); } } + + // Reads the request, then stalls well past the client's request timeout before sending a perfectly + // valid 101, so the upgrade response lands on a future the timeout has already aborted. + private int startServerWithLateHandshake(long delayMillis) throws IOException { + serverSocket = new ServerSocket(0, 1, InetAddress.getByName("localhost")); + int port = serverSocket.getLocalPort(); + serverThread = new Thread(() -> { + try (Socket socket = serverSocket.accept()) { + InputStream in = socket.getInputStream(); + StringBuilder request = new StringBuilder(); + int b3 = -1, b2 = -1, b1 = -1, b; + while ((b = in.read()) != -1) { + request.append((char) b); + if (b3 == '\r' && b2 == '\n' && b1 == '\r' && b == '\n') { + break; + } + b3 = b2; + b2 = b1; + b1 = b; + } + + // Echo back the accept value the client expects, so the only reason not to upgrade is that the + // exchange is already over. + String key = null; + for (String line : request.toString().split("\r\n")) { + if (line.regionMatches(true, 0, "Sec-WebSocket-Key:", 0, "Sec-WebSocket-Key:".length())) { + key = line.substring("Sec-WebSocket-Key:".length()).trim(); + } + } + + Thread.sleep(delayMillis); + + OutputStream out = socket.getOutputStream(); + out.write(("HTTP/1.1 101 Switching Protocols\r\n" + + "Upgrade: websocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-WebSocket-Accept: " + WebSocketUtils.getAcceptKey(key) + "\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII)); + out.flush(); + Thread.sleep(5000); + } catch (Exception ignored) { + } + }); + serverThread.setDaemon(true); + serverThread.start(); + return port; + } + + /** + * End-to-end contract: a 101 that arrives after the request timeout must never surface as onOpen. Two + * independent things uphold it - NettyRequestSender.abort marks the channel discarded before it completes + * the future, and WebSocketHandler drops an upgrade response landing on a finished future. The second is + * what covers the paths where the first does not run in that order (see + * {@code WebSocketHandlerDoneFutureTest}, which pins that guard directly). + */ + @Test(timeOut = 30000) + public void doesNotUpgradeAfterTheFutureWasAborted() throws Exception { + int port = startServerWithLateHandshake(2000); + + CountDownLatch openLatch = new CountDownLatch(1); + CountDownLatch errorLatch = new CountDownLatch(1); + + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setRequestTimeout(500) + .build(); + try (AsyncHttpClient c = asyncHttpClient(config)) { + try { + c.prepareGet("ws://localhost:" + port + "/") + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() { + @Override + public void onOpen(WebSocket websocket) { + openLatch.countDown(); + } + + @Override + public void onClose(WebSocket websocket, int code, String reason) { + } + + @Override + public void onError(Throwable t) { + errorLatch.countDown(); + } + }).build()).get(); + fail("the request must fail on the request timeout"); + } catch (ExecutionException expected) { + // TimeoutException + } + + assertTrue(errorLatch.await(2, TimeUnit.SECONDS), "onError must fire when the request times out"); + assertFalse(openLatch.await(4, TimeUnit.SECONDS), + "onOpen must not fire on a 101 that arrives after the future was aborted"); + } + } } From 5ee2841cbf268bba4a200578be3938ccfc6cc6d6 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 6 Aug 2026 21:53:26 +0000 Subject: [PATCH 20/32] Bound decompressed responses cumulatively, not per decode call Motivation: The previous attempt at this guard passed maxDecompressedResponseSize to Netty's HttpContentDecompressor(int) constructor. That neither bounds a response nor is safe. It does not bound anything. Netty's maxAllocation reaches ZlibDecoder as the maxCapacity of the buffer produced by ONE decode() call; there is no counter across calls. The HTTP codec feeds the decompressor at most httpClientCodecMaxChunkSize - 8 KiB by default - so at any ordinary compression ratio a single decode step cannot approach 256 MiB no matter how large the response becomes. A body that inflates 20:1 delivers about 160 KiB per step and passes the "cap" indefinitely. Worse, it introduced a cheaper denial of service than the one it was meant to stop. Netty 4.1's HttpContentDecompressor.newContentDecoder forwards maxAllocation to new BrotliDecoder(int), whose one-argument form is inputBufferSize, not an output ceiling, and BrotliDecoder.handlerAdded eagerly allocates new DecoderJNI.Wrapper(inputBufferSize) in direct memory. With the 268435456 default, any peer answering Content-Encoding: br - even with an empty body - would cost 256 MiB of native memory per concurrent response. Modification: Count the bytes instead. BoundedHttpContentDecompressor extends HttpContentDecompressor using the no-arg super, so maxAllocation stays at Netty's 0, and it also absorbs the keepEncodingHeader anonymous subclass as a constructor flag. The counter is appended to the embedded decoder returned by newContentDecoder rather than placed downstream in the channel pipeline. That is not a stylistic choice: HttpContentDecoder publishes inflated content by firing it at its own outer context from a forwarder appended to that same embedded pipeline, so an override of decode observes nothing - verified against netty-codec-http 4.1.136. Hooking there also gets the scope right for free, since a decoder exists only for a body actually being inflated and only for one response: an unencoded body is never counted, nothing needs resetting between responses on a pooled connection, and the channel pipeline keeps exactly the shape it had. Passing the ceiling releases the buffer and raises a DecompressionException, which AsyncHttpClientHandler turns into a failed request; a latch drops the leftovers the decoder re-runs during teardown so cleanup cannot throw a second time. AsyncHttpClientConfig.getMaxDecompressedResponseSize's default method now returns the literal instead of reading the properties file: it is reached on the event loop while the pipeline is built, so a typo in a config file would otherwise fail every connection rather than startup. Result: A response inflating past the ceiling fails whether it does so in one step or in a thousand, and no Netty decoder is ever handed a non-zero maxAllocation. Http1DecompressionLimitTest now leads with a 20:1 body that exceeds a 1 MiB ceiling only in total - asserting first that no single 8 KiB chunk could reach it, so a per-chunk limit cannot pass the test - and adds per-response-reset, keepEncodingHeader-passthrough and maxAllocation-is-zero cases. Both of those two fail against the previous commit; the older single-byte-payload cases passed there only because one chunk inflated past the cap on its own. The suite is also green under -Dio.netty.leakDetection.level=paranoid, which covers the release path taken when the ceiling trips. --- .../AsyncHttpClientConfig.java | 14 +- .../netty/channel/ChannelManager.java | 29 +-- .../BoundedHttpContentDecompressor.java | 120 +++++++++++ .../Http1DecompressionLimitTest.java | 189 +++++++++++++++--- 4 files changed, 302 insertions(+), 50 deletions(-) create mode 100644 client/src/main/java/org/asynchttpclient/netty/handler/BoundedHttpContentDecompressor.java diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index 7711337510..3a22946e27 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -292,7 +292,6 @@ public interface AsyncHttpClientConfig { boolean isKeepEncodingHeader(); - int getShutdownQuietPeriod(); int getShutdownTimeout(); @@ -359,14 +358,19 @@ default boolean isStripAuthorizationOnRedirect() { } /** - * The maximum number of bytes a compressed response body may inflate to. Guards against a - * "decompression bomb": a small response that expands without bound once decoded. {@code 0} disables the - * limit and restores Netty's unbounded behaviour. + * The maximum number of bytes a compressed response body may inflate to, counted over the whole + * response. Guards against a "decompression bomb": a small response that expands without bound once + * decoded. {@code 0} disables the limit. + *

+ * The literal is hardcoded rather than read through {@link AsyncHttpClientConfigDefaults}: this default + * runs for any third-party implementation of this interface, and it is reached on the event loop when + * the pipeline is built, so a property lookup plus {@code Integer.parseInt} would turn a typo in a + * configuration file into a failure on every connection rather than at startup. * * @return the decompressed response ceiling in bytes */ default int getMaxDecompressedResponseSize() { - return AsyncHttpClientConfigDefaults.defaultMaxDecompressedResponseSize(); + return 268435456; } enum ResponseBodyPartFactory { diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index 0abd05ec8f..caa3a56dd3 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -23,7 +23,6 @@ import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.http.HttpClientCodec; -import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder; import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder; import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator; @@ -46,6 +45,7 @@ import org.asynchttpclient.netty.NettyResponseFuture; import org.asynchttpclient.netty.OnLastHttpContentCallback; import org.asynchttpclient.netty.handler.AsyncHttpClientHandler; +import org.asynchttpclient.netty.handler.BoundedHttpContentDecompressor; import org.asynchttpclient.netty.handler.HttpHandler; import org.asynchttpclient.netty.handler.WebSocketHandler; import org.asynchttpclient.netty.request.NettyRequestSender; @@ -253,29 +253,14 @@ protected void initChannel(Channel ch) { }); } - private HttpContentDecompressor newHttpContentDecompressor() { - // Bound the decompressor's cumulative allocation to guard against decompression bombs (a small - // compressed body that inflates without limit). The no-arg constructor uses maxAllocation=0, i.e. - // unbounded; Netty throws a DecompressionException once the inflated size would exceed the ceiling. - int maxAllocation = maxDecompressedResponseSize(); - if (config.isKeepEncodingHeader()) - return new HttpContentDecompressor(maxAllocation) { - @Override - protected String getTargetContentEncoding(String contentEncoding) { - return contentEncoding; - } - }; - else - return new HttpContentDecompressor(maxAllocation); - } - /** - * The configured decompressed-response ceiling, normalised for Netty's maxAllocation parameter: any - * non-positive value means "no limit", which is what {@code 0} tells Netty. + * A decompressor whose output is counted over the whole response, failing the request once it exceeds + * {@link AsyncHttpClientConfig#getMaxDecompressedResponseSize()}, so a small but highly compressible body + * cannot inflate without bound. Netty's own {@code maxAllocation} parameter is deliberately left at 0 - + * see {@link BoundedHttpContentDecompressor} for why it neither bounds a response nor is safe to set. */ - private int maxDecompressedResponseSize() { - int configured = config.getMaxDecompressedResponseSize(); - return configured > 0 ? configured : 0; + private BoundedHttpContentDecompressor newHttpContentDecompressor() { + return new BoundedHttpContentDecompressor(config.isKeepEncodingHeader(), config.getMaxDecompressedResponseSize()); } public final void tryToOfferChannelToPool(Channel channel, AsyncHandler asyncHandler, boolean keepAlive, Object partitionKey) { diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/BoundedHttpContentDecompressor.java b/client/src/main/java/org/asynchttpclient/netty/handler/BoundedHttpContentDecompressor.java new file mode 100644 index 0000000000..02bb314bdf --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/handler/BoundedHttpContentDecompressor.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.compression.DecompressionException; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.util.ReferenceCountUtil; + +/** + * An {@link HttpContentDecompressor} that bounds how far a response body may inflate, guarding against a + * decompression bomb: a small, highly compressible response that expands without limit once decoded and + * exhausts the client's heap. + * + *

The bound is cumulative over one response body, and that is the whole point. Netty's + * own {@code maxAllocation} constructor parameter is not a substitute: it reaches {@code ZlibDecoder} as + * the maxCapacity of the buffer produced by one {@code decode()} call, with no counter spanning + * calls. Since the HTTP codec hands the decompressor at most + * {@link org.asynchttpclient.AsyncHttpClientConfig#getHttpClientCodecMaxChunkSize()} bytes at a time + * (8 KiB by default), a body at any ordinary compression ratio never reaches it however large the total + * becomes. Setting a non-zero {@code maxAllocation} is also actively harmful here: Netty 4.1's + * {@code HttpContentDecompressor.newContentDecoder} forwards it to {@code new BrotliDecoder(int)}, whose + * single-argument form is an input buffer size, and {@code BrotliDecoder.handlerAdded} eagerly + * allocates a native {@code DecoderJNI.Wrapper} of exactly that size - so a 256 MiB "ceiling" would cost + * 256 MiB of direct memory for every {@code Content-Encoding: br} response, empty body included. This + * class leaves {@code maxAllocation} at Netty's default of 0 and counts the bytes itself. + * + *

The counter is installed inside the decoder Netty builds per response, at the end of that + * embedded pipeline, rather than downstream of this handler. {@code HttpContentDecoder} publishes inflated + * content by firing it at its own outer {@link ChannelHandlerContext} from a forwarder appended to that + * same embedded pipeline, so an override of {@code decode} would observe nothing. Hooking here also gets + * the scope right for free: a decoder exists only for a body actually being inflated, and only for one + * response, so an unencoded body is never counted (bounding those would fail large plain downloads the + * caller asked for) and nothing has to be reset between responses on a pooled connection. + * + *

Not shareable: it holds the state of one connection's decoder. + */ +public class BoundedHttpContentDecompressor extends HttpContentDecompressor { + + private final boolean keepEncodingHeader; + /** + * Ceiling in bytes for a single response body; a non-positive value disables the check. + */ + private final long maxDecompressedBytes; + + public BoundedHttpContentDecompressor(boolean keepEncodingHeader, long maxDecompressedBytes) { + // Deliberately the no-arg super: see the class javadoc for why maxAllocation must stay 0. + super(); + this.keepEncodingHeader = keepEncodingHeader; + this.maxDecompressedBytes = maxDecompressedBytes; + } + + @Override + protected EmbeddedChannel newContentDecoder(String contentEncoding) throws Exception { + EmbeddedChannel decoder = super.newContentDecoder(contentEncoding); + if (decoder != null && maxDecompressedBytes > 0) { + // Appended before HttpContentDecoder appends its own forwarder, so this sits between the + // decompressor and the forwarder and sees every inflated buffer before it leaves the decoder. + decoder.pipeline().addLast(new DecompressedSizeLimit()); + } + return decoder; + } + + @Override + protected String getTargetContentEncoding(String contentEncoding) throws Exception { + // keepEncodingHeader leaves the response advertising the encoding it arrived with, for callers that + // inspect it; the default rewrites it to "identity" because the body downstream is no longer encoded. + return keepEncodingHeader ? contentEncoding : super.getTargetContentEncoding(contentEncoding); + } + + /** + * Counts the inflated output of the response this decoder was created for, and fails the exchange once + * it passes the ceiling. + */ + private final class DecompressedSizeLimit extends ChannelInboundHandlerAdapter { + + private long decompressedBytes; + private boolean exceeded; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (exceeded) { + // Already failed. Tearing the decoder down calls finishAndReleaseAll(), which re-runs it over + // whatever is left cumulated and would throw a second time out of cleanup; drop the leftovers + // quietly instead so the caller sees the exception raised below. + ReferenceCountUtil.release(msg); + return; + } + + if (msg instanceof ByteBuf) { + decompressedBytes += ((ByteBuf) msg).readableBytes(); + if (decompressedBytes > maxDecompressedBytes) { + exceeded = true; + ReferenceCountUtil.release(msg); + // EmbeddedChannel rethrows this out of the writeInbound driving the decoder, inside + // HttpContentDecoder.decode; being a DecoderException it reaches AsyncHttpClientHandler's + // exceptionCaught unwrapped, which fails the request and closes the connection. + throw new DecompressionException("Decompressed response body exceeds the maximum of " + + maxDecompressedBytes + " bytes (" + decompressedBytes + " bytes decompressed so far)"); + } + } + + ctx.fireChannelRead(msg); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java b/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java index a46c92f71c..894a6713ab 100644 --- a/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java +++ b/client/src/test/java/org/asynchttpclient/Http1DecompressionLimitTest.java @@ -17,14 +17,23 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import io.netty.handler.codec.compression.DecompressionException; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timer; +import org.asynchttpclient.netty.channel.ChannelManager; +import org.asynchttpclient.netty.handler.BoundedHttpContentDecompressor; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.Random; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.zip.GZIPOutputStream; @@ -34,27 +43,72 @@ import static org.testng.Assert.fail; /** - * Verifies that HTTP/1.1 automatic response decompression honours the configured decompressed-size ceiling - * ({@link AsyncHttpClientConfig#getMaxDecompressedResponseSize()}), so a highly compressible body cannot - * inflate without bound (a "decompression bomb"). Before the fix both {@code HttpContentDecompressor} - * construction sites used the no-arg constructor (maxAllocation=0 = unbounded). See - * {@code ChannelManager#newHttpContentDecompressor}. + * HTTP/1.1 automatic response decompression must honour + * {@link AsyncHttpClientConfig#getMaxDecompressedResponseSize()} cumulatively over the whole + * response, so a small, highly compressible body cannot inflate without bound. + * + *

The distinction matters. Netty's {@code HttpContentDecompressor(int maxAllocation)} caps the output + * of one {@code ZlibDecoder.decode()} call and keeps no counter across calls, and the HTTP codec + * feeds the decompressor at most {@code httpClientCodecMaxChunkSize} (8 KiB) at a time. A body with a + * merely ordinary compression ratio therefore sails straight through such a "limit" no matter how large + * its total becomes - which is what {@link #cumulativeLimitFiresEvenWhenNoSingleChunkExceedsIt()} pins + * down. Passing a non-zero maxAllocation is also unsafe: Netty 4.1 forwards it to + * {@code new BrotliDecoder(int)}, an input buffer size that is eagerly allocated as native + * memory, so a 256 MiB "ceiling" would mean a 256 MiB direct allocation per {@code br} response - see + * {@link #decompressorIsBuiltWithoutANettyMaxAllocation()}. */ public class Http1DecompressionLimitTest { // 4 MiB of a single repeated byte: gzips to a few KiB but inflates to 4 MiB. private static final byte[] LARGE_PAYLOAD = new byte[4 * 1024 * 1024]; + /** + * 8 MiB that gzips at a ratio of roughly 20:1. Deliberately NOT a run of one byte: at 20:1 the largest + * amount a single 8 KiB codec chunk can inflate to is about 160 KiB, which is far below both the 1 MiB + * ceiling this payload is served against and Netty's 256 MiB maxAllocation default. Only a counter that + * spans the whole response can stop it. + */ + private static final byte[] RATIO_20_PAYLOAD = ratio20Payload(8 * 1024 * 1024); + + private static final int LOW_RATIO_LIMIT = 1024 * 1024; + static { Arrays.fill(LARGE_PAYLOAD, (byte) 'a'); } + private static byte[] ratio20Payload(int size) { + byte[] bytes = new byte[size]; + Arrays.fill(bytes, (byte) 'a'); + Random random = new Random(42); // fixed seed: the compression ratio must not vary between runs + for (int i = 0; i < size; i++) { + if (random.nextInt(48) == 0) { + bytes[i] = (byte) ('b' + random.nextInt(20)); + } + } + return bytes; + } + + private static byte[] gzip(byte[] payload) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + GZIPOutputStream gzip = new GZIPOutputStream(out); + gzip.write(payload); + gzip.finish(); + gzip.close(); + return out.toByteArray(); + } + private static HttpServer httpServer; @BeforeClass public static void setupServer() throws Exception { httpServer = HttpServer.create(new InetSocketAddress("localhost", 0), 0); - httpServer.createContext("/gzip-bomb").setHandler(new HttpHandler() { + httpServer.createContext("/gzip-bomb").setHandler(gzipHandler(LARGE_PAYLOAD)); + httpServer.createContext("/gzip-ratio-20").setHandler(gzipHandler(RATIO_20_PAYLOAD)); + httpServer.start(); + } + + private static HttpHandler gzipHandler(final byte[] payload) { + return new HttpHandler() { @Override public void handle(HttpExchange exchange) throws IOException { exchange.getResponseHeaders().set("Content-Encoding", "gzip"); @@ -62,15 +116,14 @@ public void handle(HttpExchange exchange) throws IOException { OutputStream out = exchange.getResponseBody(); try { GZIPOutputStream gzip = new GZIPOutputStream(out); - gzip.write(LARGE_PAYLOAD); + gzip.write(payload); gzip.finish(); gzip.close(); } finally { out.close(); } } - }); - httpServer.start(); + }; } @AfterClass @@ -93,8 +146,8 @@ private static AsyncHttpClient clientWithLimit(int maxDecompressedResponseSize, return new DefaultAsyncHttpClient(config); } - private static String url() { - return "http://localhost:" + httpServer.getAddress().getPort() + "/gzip-bomb"; + private static String url(String path) { + return "http://localhost:" + httpServer.getAddress().getPort() + path; } private static boolean hasDecompressionCause(Throwable t) { @@ -106,17 +159,45 @@ private static boolean hasDecompressionCause(Throwable t) { return false; } + /** + * The core of the fix. The body inflates to 8 MiB against a 1 MiB ceiling, but no single decode step + * produces more than about 160 KiB, so nothing that looks only at one chunk - including Netty's + * maxAllocation - can catch it. The arithmetic is asserted, not assumed, so the payload cannot silently + * drift into being a trivially compressible one. + */ + @Test + public void cumulativeLimitFiresEvenWhenNoSingleChunkExceedsIt() throws Exception { + byte[] compressed = gzip(RATIO_20_PAYLOAD); + double ratio = (double) RATIO_20_PAYLOAD.length / compressed.length; + long largestSingleChunkInflation = (long) Math.ceil(ratio * 8192); + + assertTrue(RATIO_20_PAYLOAD.length > LOW_RATIO_LIMIT, + "the payload must exceed the ceiling in total, else the test proves nothing"); + assertTrue(largestSingleChunkInflation < LOW_RATIO_LIMIT, + "no single 8 KiB codec chunk may reach the ceiling on its own, else a per-chunk limit would " + + "also pass this test; ratio=" + ratio + ", per-chunk=" + largestSingleChunkInflation); + + try (AsyncHttpClient client = clientWithLimit(LOW_RATIO_LIMIT)) { + try { + client.prepareGet(url("/gzip-ratio-20")).execute().get(30, TimeUnit.SECONDS); + fail("a body inflating past the configured ceiling must not be delivered, even in small steps"); + } catch (ExecutionException ex) { + assertTrue(hasDecompressionCause(ex), + "expected a DecompressionException in the cause chain but got: " + ex.getCause()); + } + } + } + @Test public void decompressionBeyondLimitFails() throws Exception { - // 256 KiB ceiling, but the body inflates to 4 MiB -> Netty must abort with a DecompressionException - // rather than allocating the full 4 MiB. + // 256 KiB ceiling, but the body inflates to 4 MiB. try (AsyncHttpClient client = clientWithLimit(256 * 1024)) { try { - client.prepareGet(url()).execute().get(30, TimeUnit.SECONDS); + client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); fail("a body inflating past the configured ceiling must not be delivered"); } catch (ExecutionException ex) { assertTrue(hasDecompressionCause(ex), - "expected a Netty DecompressionException in the cause chain but got: " + ex.getCause()); + "expected a DecompressionException in the cause chain but got: " + ex.getCause()); } } } @@ -125,35 +206,97 @@ public void decompressionBeyondLimitFails() throws Exception { public void decompressionWithinLimitSucceeds() throws Exception { // A generous ceiling comfortably above the 4 MiB inflated size lets the same body through unchanged. try (AsyncHttpClient client = clientWithLimit(64 * 1024 * 1024)) { - Response response = client.prepareGet(url()).execute().get(30, TimeUnit.SECONDS); + Response response = client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); assertEquals(response.getStatusCode(), 200); assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length); } } + /** + * The counter must restart per response, so a client whose ceiling is above a single body's inflated + * size can serve an unbounded number of them on a pooled connection. + */ + @Test + public void limitIsPerResponseNotPerConnection() throws Exception { + try (AsyncHttpClient client = clientWithLimit(8 * 1024 * 1024)) { + for (int i = 0; i < 3; i++) { + Response response = client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length, + "response " + i + " must decompress fully; the counter must not carry over"); + } + } + } + @Test public void decompressionBeyondLimitFailsWithKeepEncodingHeader() throws Exception { - // keepEncodingHeader selects the anonymous HttpContentDecompressor subclass, a separate construction - // site that has to forward the ceiling to super. Covers the half-fix where only the plain branch is - // bounded. + // keepEncodingHeader used to select a separate anonymous HttpContentDecompressor subclass; it is now a + // constructor flag, and this covers the half-fix where only one of the two branches was bounded. try (AsyncHttpClient client = clientWithLimit(256 * 1024, true)) { try { - client.prepareGet(url()).execute().get(30, TimeUnit.SECONDS); + client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); fail("the keepEncodingHeader decompressor must honour the ceiling too"); } catch (ExecutionException ex) { assertTrue(hasDecompressionCause(ex), - "expected a Netty DecompressionException in the cause chain but got: " + ex.getCause()); + "expected a DecompressionException in the cause chain but got: " + ex.getCause()); } } } + @Test + public void keepEncodingHeaderStillKeepsTheEncodingHeader() throws Exception { + try (AsyncHttpClient client = clientWithLimit(64 * 1024 * 1024, true)) { + Response response = client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); + assertEquals(response.getStatusCode(), 200); + assertEquals(response.getHeader("Content-Encoding"), "gzip", + "keepEncodingHeader must leave the response advertising the encoding it arrived with"); + assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length); + } + } + @Test public void zeroDisablesTheLimit() throws Exception { - // 0 keeps Netty's unbounded behaviour for callers that deliberately opt out. + // 0 keeps the unbounded behaviour for callers that deliberately opt out. try (AsyncHttpClient client = clientWithLimit(0)) { - Response response = client.prepareGet(url()).execute().get(30, TimeUnit.SECONDS); + Response response = client.prepareGet(url("/gzip-bomb")).execute().get(30, TimeUnit.SECONDS); assertEquals(response.getStatusCode(), 200); assertEquals(response.getResponseBodyAsBytes().length, LARGE_PAYLOAD.length); } } + + /** + * brotli4j is not on the 2.x classpath, so a {@code br} response cannot be exercised end to end here. + * What can be pinned - and is the part that matters - is that AHC never hands Netty a non-zero + * maxAllocation. In Netty 4.1, {@code HttpContentDecompressor.newContentDecoder} passes maxAllocation to + * {@code new BrotliDecoder(int)}, whose one-argument form is {@code inputBufferSize}, and + * {@code BrotliDecoder.handlerAdded} eagerly allocates {@code new DecoderJNI.Wrapper(inputBufferSize)} in + * direct memory. With maxAllocation left at 256 MiB, any peer that answers + * {@code Content-Encoding: br} - even with an empty body - would cost the client 256 MiB of native + * memory per concurrent response: cheaper for an attacker than the bomb this limit exists to stop. + */ + @Test + public void decompressorIsBuiltWithoutANettyMaxAllocation() throws Exception { + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setMaxDecompressedResponseSize(256 * 1024 * 1024) + .build(); + Timer timer = new HashedWheelTimer(); + ChannelManager channelManager = new ChannelManager(config, timer); + try { + Method factory = ChannelManager.class.getDeclaredMethod("newHttpContentDecompressor"); + factory.setAccessible(true); + HttpContentDecompressor decompressor = (HttpContentDecompressor) factory.invoke(channelManager); + + assertTrue(decompressor instanceof BoundedHttpContentDecompressor, + "the pipeline must use the counting decompressor, not a bare Netty one: " + decompressor.getClass()); + + Field maxAllocation = HttpContentDecompressor.class.getDeclaredField("maxAllocation"); + maxAllocation.setAccessible(true); + assertEquals(maxAllocation.getInt(decompressor), 0, + "Netty's maxAllocation must stay 0: it does not bound a response, and a non-zero value is " + + "used as the eagerly allocated BrotliDecoder input buffer size"); + } finally { + channelManager.close(); + timer.stop(); + } + } } From 3a625cb892233c0a6653ac68823a25ffbc80f393 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 6 Aug 2026 22:10:59 +0000 Subject: [PATCH 21/32] Stop a rejected CONNECT being treated as an established tunnel Motivation: NettyRequestSender.isConnectAlreadyDone inferred "the tunnel is up" from "the last request we built was a CONNECT". That is equally true when the proxy REJECTED the CONNECT: ConnectSuccessInterceptor never ran, no TLS was ever negotiated, and the socket is still a plaintext hop terminated by the proxy. Meanwhile Interceptors.exitAfterIntercept dispatched the proxy's rejection to the ORIGIN-request interceptors. A 401 went to Unauthorized401Interceptor and a 3xx to Redirect30xInterceptor, both of which rebuild the exchange as the origin request with setReuseChannel(true); because that request is not a CONNECT, NettyRequestFactory then attached the origin's Authorization to it. The result, reproduced on the shipping build: CONNECT origin:443 with no credentials, correctly, and then on the SAME plaintext socket "GET /secret" carrying "authorization: Basic ". A hostile or merely compromised proxy needed nothing but a 401 - which any non-preemptive realm, the default, will answer - or a 302, or a 401 NTLM challenge it chooses itself, to harvest them. Separately, ProxyUnauthorized407Interceptor had no proxy-type guard. A SOCKS proxy negotiates at the transport layer and never emits an HTTP status line, so a 407 seen over one was written by the ORIGIN. Its NTLM and Kerberos/SPNEGO branches write Proxy-Authorization directly onto the request headers, which newNettyRequest copies verbatim, so this path bypassed the proxy-type gates added earlier to the preemptive paths and minted proxy credentials for the origin. Modification: Track the tunnel explicitly. NettyResponseFuture gains tunnelEstablished, set only by ConnectSuccessInterceptor and cleared whenever a CONNECT is attached to the future, and isConnectAlreadyDone now requires it. Route CONNECT responses separately: a CONNECT is addressed to the proxy, so only a 200 (tunnel established) and a 407 (proxy challenge) may be acted on. Everything else is returned to the caller untouched, and the cookie store is not updated either - currentRequest is the ORIGIN request, so a Set-Cookie on a rejected CONNECT would otherwise be filed against an origin the proxy never reached. Add the missing proxy-type guard to exitAfterHandling407. Result: A proxy that does not establish the tunnel is only ever sent CONNECTs, and its rejection is surfaced to the caller as the response. Added ConnectTunnelStateTest, whose fake proxy records the actual bytes: on the previous commit it captures "authorization: Basic b3JpZ2luLXVzZXI6b3JpZ2luLXNlY3JldA==" on the 401 path and a bare "GET /secret2" on the 302 path, and both now send nothing but CONNECTs. Its third case is the regression guard - after a 200 the CONNECT carried Proxy-Authorization and the tunnelled request carried the origin's Authorization, before and after. Added ProxyUnauthorized407InterceptorTest for the SOCKS4/SOCKS5/no-proxy rejections and the HTTP-proxy path that must still work. HttpsProxyTest, ProxyTest, CustomHeaderProxyTest, NTLMProxyTest, ProxyTunnellingTest and the auth suites are unchanged and green. --- .../netty/NettyResponseFuture.java | 26 ++ .../intercept/ConnectSuccessInterceptor.java | 4 + .../netty/handler/intercept/Interceptors.java | 26 +- .../ProxyUnauthorized407Interceptor.java | 9 + .../netty/request/NettyRequestSender.java | 5 + .../ProxyUnauthorized407InterceptorTest.java | 190 +++++++++++ .../proxy/ConnectTunnelStateTest.java | 313 ++++++++++++++++++ 7 files changed, 569 insertions(+), 4 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java create mode 100644 client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java index dddebfff41..68edc75bc5 100755 --- a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java +++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java @@ -14,6 +14,7 @@ package org.asynchttpclient.netty; import io.netty.channel.Channel; +import io.netty.handler.codec.http.HttpMethod; import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.ListenableFuture; import org.asynchttpclient.Realm; @@ -116,6 +117,7 @@ public final class NettyResponseFuture implements ListenableFuture { private boolean headersAlreadyWrittenOnContinue; private boolean dontWriteBodyBecauseExpectContinue; private boolean allowConnect; + private boolean tunnelEstablished; private Realm realm; private Realm proxyRealm; @@ -338,9 +340,33 @@ public final NettyRequest getNettyRequest() { } public final void setNettyRequest(NettyRequest nettyRequest) { + if (nettyRequest != null && nettyRequest.getHttpRequest().method() == HttpMethod.CONNECT) { + // A new tunnel attempt is starting, so whatever an earlier CONNECT established no longer holds. + // Only ConnectSuccessInterceptor may set this back to true. Keeping the invariant here rather than + // at the call site means no future path can attach a CONNECT while leaving the flag stale. + tunnelEstablished = false; + } this.nettyRequest = nettyRequest; } + /** + * Whether a CONNECT on the channel this exchange is using actually succeeded, i.e. whether the socket is + * a tunnel to the origin rather than a plaintext hop to the proxy. Set only by + * {@link org.asynchttpclient.netty.handler.intercept.ConnectSuccessInterceptor}. The fact that the last + * request sent was a CONNECT proves nothing on its own: it is equally true when the proxy REJECTED the + * CONNECT, and treating that as an established tunnel puts the origin's credentials on a socket the + * proxy is still reading in the clear. + * + * @return true if a CONNECT has been answered successfully for the current channel + */ + public boolean isTunnelEstablished() { + return tunnelEstablished; + } + + public void setTunnelEstablished(boolean tunnelEstablished) { + this.tunnelEstablished = tunnelEstablished; + } + public final AsyncHandler getAsyncHandler() { return asyncHandler; } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java index eb2e98e36f..681e425d9f 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java @@ -45,6 +45,10 @@ public boolean exitAfterHandlingConnect(Channel channel, if (future.isKeepAlive()) future.attachChannel(channel, true); + // The single place this may be set. From here on the socket is a tunnel to the origin, so the origin's + // credentials may travel on it; until this point it is a plaintext hop to the proxy. + future.setTunnelEstablished(true); + Uri requestUri = request.getUri(); LOGGER.debug("Connecting to proxy {} for scheme {}", proxyServer, requestUri.getScheme()); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java index d0d6dc71fe..8b52427bf7 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Interceptors.java @@ -73,9 +73,15 @@ public boolean exitAfterIntercept(Channel channel, // redirect target whose auth was just stripped, leaking them to a different origin that answers 401. Realm realm = future.getRealm(); + // A CONNECT is addressed to the PROXY and travels in the clear, so its response is the proxy's, not + // the origin's - which changes both what may be stored from it and who may act on it. + boolean connectRequest = httpRequest.method() == HttpMethod.CONNECT; + // This MUST BE called before Redirect30xInterceptor because latter assumes cookie store is already updated CookieStore cookieStore = config.getCookieStore(); - if (cookieStore != null) { + if (cookieStore != null && !connectRequest) { + // Skipped on a CONNECT: currentRequest is the ORIGIN request, so a Set-Cookie in the proxy's answer + // would be filed against the URI of an origin the request never reached. for (String cookieStr : responseHeaders.getAll(SET_COOKIE)) { Cookie c = cookieDecoder.decode(cookieStr); if (c != null) { @@ -89,6 +95,21 @@ public boolean exitAfterIntercept(Channel channel, return true; } + // Only two answers to a CONNECT may be acted on: a 200 that establishes the tunnel, and a 407 asking + // the proxy realm for credentials. Everything else - 401, 3xx, 100 - must NOT reach the origin-request + // interceptors, which rebuild the exchange as the ORIGIN request with setReuseChannel(true). That put + // the origin's Authorization on a socket still terminated by the proxy in plaintext, and let a hostile + // or compromised proxy solicit it with nothing more than a 401 or a 302. + if (connectRequest) { + if (statusCode == PROXY_AUTHENTICATION_REQUIRED_407) { + return proxyUnauthorized407Interceptor.exitAfterHandling407(channel, future, response, request, proxyServer, httpRequest); + } + if (statusCode == OK_200) { + return connectSuccessInterceptor.exitAfterHandlingConnect(channel, future, request, proxyServer); + } + return false; + } + if (statusCode == UNAUTHORIZED_401) { return unauthorized401Interceptor.exitAfterHandling401(channel, future, response, request, realm, httpRequest); @@ -101,9 +122,6 @@ public boolean exitAfterIntercept(Channel channel, } else if (Redirect30xInterceptor.REDIRECT_STATUSES.contains(statusCode)) { return redirect30xInterceptor.exitAfterHandlingRedirect(channel, future, response, request, statusCode, realm); - } else if (httpRequest.method() == HttpMethod.CONNECT && statusCode == OK_200) { - return connectSuccessInterceptor.exitAfterHandlingConnect(channel, future, request, proxyServer); - } return false; } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java index 57436e9ae5..ec2dfdeef1 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java @@ -70,6 +70,15 @@ public boolean exitAfterHandling407(Channel channel, return false; } + // A SOCKS proxy tunnels at the transport layer and never speaks HTTP, so a 407 arriving over one was + // written by the ORIGIN. Answering it mints proxy credentials for the origin: the NTLM and + // Kerberos/SPNEGO branches below write Proxy-Authorization straight onto the request headers, which + // newNettyRequest then copies verbatim, bypassing the proxy-type gate that guards the preemptive path. + if (proxyServer == null || !proxyServer.getProxyType().isHttp()) { + LOGGER.debug("Can't handle 407: not an HTTP proxy, so the 407 came from the origin"); + return false; + } + List proxyAuthHeaders = response.headers().getAll(PROXY_AUTHENTICATE); if (proxyAuthHeaders.isEmpty()) { diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index f2a8038d77..c327e14c90 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -117,6 +117,11 @@ public ListenableFuture sendRequest(final Request request, private boolean isConnectAlreadyDone(Request request, NettyResponseFuture future) { return future != null + // The last request having been a CONNECT proves nothing: it is equally true when the proxy + // REJECTED it with 401, 407 or a redirect. ConnectSuccessInterceptor never ran in that case, + // so the socket is still a plaintext hop to the proxy. Skipping the CONNECT here would send + // the ORIGIN request - Authorization header and all - straight down it. + && future.isTunnelEstablished() && future.getNettyRequest() != null && future.getNettyRequest().getHttpRequest().method() == HttpMethod.CONNECT && !request.getMethod().equals(CONNECT); diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java new file mode 100644 index 0000000000..4df18bc7fc --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.handler.intercept; + +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.util.HashedWheelTimer; +import io.netty.util.Timer; +import org.asynchttpclient.AsyncHandler; +import org.asynchttpclient.AsyncHttpClientConfig; +import org.asynchttpclient.AsyncHttpClientState; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.Response; +import org.asynchttpclient.channel.ChannelPoolPartitioning; +import org.asynchttpclient.AsyncCompletionHandlerBase; +import org.asynchttpclient.netty.NettyResponseFuture; +import org.asynchttpclient.netty.channel.ChannelManager; +import org.asynchttpclient.netty.request.NettyRequest; +import org.asynchttpclient.netty.request.NettyRequestSender; +import org.asynchttpclient.proxy.ProxyServer; +import org.asynchttpclient.proxy.ProxyType; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.lang.reflect.Constructor; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A 407 is an HTTP-proxy mechanism. A SOCKS proxy negotiates at the transport layer and never writes an + * HTTP status line, so a 407 arriving over one was written by the ORIGIN - and answering it hands the + * proxy's credentials to that origin. + * + *

The preemptive path is already gated by proxy type in NettyRequestFactory and NettyRequestSender (see + * {@code SocksProxyCredentialLeakTest}), but this interceptor bypassed both: its NTLM and Kerberos/SPNEGO + * branches write {@code Proxy-Authorization} straight onto the request headers, and {@code newNettyRequest} + * copies request headers verbatim afterwards. + */ +public class ProxyUnauthorized407InterceptorTest { + + private Timer timer; + private ChannelManager channelManager; + private ProxyUnauthorized407Interceptor interceptor; + private EmbeddedChannel channel; + + private static AsyncHttpClientState newClientState() throws Exception { + Constructor constructor = AsyncHttpClientState.class.getDeclaredConstructor(AtomicBoolean.class); + constructor.setAccessible(true); + return constructor.newInstance(new AtomicBoolean(false)); + } + + private static NettyRequest newNettyRequest(HttpRequest httpRequest) throws Exception { + Constructor constructor = NettyRequest.class.getDeclaredConstructor(HttpRequest.class, + Class.forName("org.asynchttpclient.netty.request.body.NettyBody")); + constructor.setAccessible(true); + return constructor.newInstance(httpRequest, null); + } + + @BeforeMethod + public void setUp() throws Exception { + AsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build(); + timer = new HashedWheelTimer(); + channelManager = new ChannelManager(config, timer); + NettyRequestSender requestSender = new NettyRequestSender(config, channelManager, timer, newClientState()); + interceptor = new ProxyUnauthorized407Interceptor(channelManager, requestSender); + channel = new EmbeddedChannel(); + } + + @AfterMethod + public void tearDown() { + if (channel != null) { + channel.finishAndReleaseAll(); + } + if (channelManager != null) { + channelManager.close(); + } + if (timer != null) { + timer.stop(); + } + } + + private static Realm nonPreemptiveProxyRealm() { + return basicAuthRealm("proxy-user", "proxy-secret").build(); + } + + private NettyResponseFuture newFuture(ProxyServer proxyServer, Realm proxyRealm) throws Exception { + Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); + HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/resource"); + AsyncHandler handler = new AsyncCompletionHandlerBase(); + + NettyResponseFuture future = new NettyResponseFuture<>(request, + handler, + newNettyRequest(httpRequest), + 0, + ChannelPoolPartitioning.PerHostChannelPoolPartitioning.INSTANCE, + null, + proxyServer); + future.setProxyRealm(proxyRealm); + return future; + } + + private static HttpResponse new407() { + HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED); + response.headers().set(HttpHeaderNames.PROXY_AUTHENTICATE, "Basic realm=\"proxy\""); + return response; + } + + private boolean handle407(ProxyServer proxyServer, Realm proxyRealm) throws Exception { + NettyResponseFuture future = newFuture(proxyServer, proxyRealm); + return interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + proxyServer, + future.getNettyRequest().getHttpRequest()); + } + + @Test + public void socks5Proxy407IsNotAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer socks = proxyServer("proxy.example.com", 1080).setProxyType(ProxyType.SOCKS_V5).setRealm(proxyRealm).build(); + + assertFalse(handle407(socks, proxyRealm), + "a 407 over a SOCKS proxy came from the origin and must not be answered with proxy credentials"); + } + + @Test + public void socks4Proxy407IsNotAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer socks = proxyServer("proxy.example.com", 1080).setProxyType(ProxyType.SOCKS_V4).setRealm(proxyRealm).build(); + + assertFalse(handle407(socks, proxyRealm), + "a 407 over a SOCKS proxy came from the origin and must not be answered with proxy credentials"); + } + + @Test + public void noProxyAtAllMeans407CameFromTheOrigin() throws Exception { + // A realm can be carried on the future without a ProxyServer being in play at all. + assertFalse(handle407(null, nonPreemptiveProxyRealm()), + "with no proxy configured a 407 can only have come from the origin"); + } + + /** + * The regression guard: an HTTP proxy's 407 is still answered, and the proxy realm is switched to + * preemptive so the retried request carries Proxy-Authorization. + */ + @Test + public void httpProxy407IsStillAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer http = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(http, proxyRealm); + + boolean handled = interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + http, + future.getNettyRequest().getHttpRequest()); + + assertTrue(handled, "an HTTP proxy's 407 must still be answered"); + assertTrue(future.getProxyRealm().isUsePreemptiveAuth(), + "the retried request must carry the proxy credentials"); + } +} diff --git a/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java b/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java new file mode 100644 index 0000000000..494cbe8d41 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java @@ -0,0 +1,313 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.proxy; + +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.Response; +import org.asynchttpclient.ws.WebSocketUpgradeHandler; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Test; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.asynchttpclient.Dsl.basicAuthRealm; +import static org.asynchttpclient.Dsl.proxyServer; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * A CONNECT is sent to the proxy over a socket that is still plaintext; the tunnel exists only once the + * proxy has answered 2xx. AHC used to infer "the tunnel is up" from nothing more than "the last request was + * a CONNECT", which is equally true when the proxy REJECTED it. On a 401 or a redirect the origin-request + * interceptors then rebuilt the exchange as the ORIGIN request with {@code setReuseChannel(true)}, and + * because that request is not a CONNECT the request factory attached the origin's {@code Authorization} to + * it - handing the origin's credentials to the proxy, in the clear, on demand. A proxy needed only to + * answer 401 (a non-preemptive realm, which is the default) or 302 to collect them. + * + *

The proxy here is a raw socket that records every request head it is sent, so the assertion is made on + * the actual bytes on the wire rather than on any client-side state. + */ +public class ConnectTunnelStateTest { + + private static final String ORIGIN_USER = "origin-user"; + private static final String ORIGIN_PASSWORD = "origin-secret"; + // Base64("origin-user:origin-secret"), i.e. what a leak looks like on the wire. + private static final String ORIGIN_BASIC = "b3JpZ2luLXVzZXI6b3JpZ2luLXNlY3JldA=="; + + private RecordingProxy proxy; + + @AfterMethod(alwaysRun = true) + public void tearDown() throws IOException { + if (proxy != null) { + proxy.close(); + proxy = null; + } + } + + /** + * A minimal HTTP proxy that answers each request head it receives with the next scripted response and + * keeps the connection open, recording everything the client sends on it afterwards. + */ + private static final class RecordingProxy implements Closeable { + + private final ServerSocket serverSocket; + private final String[] responses; + private final List requestHeads = new CopyOnWriteArrayList<>(); + /** + * Counts down to the number of request heads the UNFIXED client would send, so a run that reproduces + * the leak does not have to wait out a request timeout to record it. + */ + private final CountDownLatch expectedHeads; + private final Thread thread; + + RecordingProxy(int expectedHeads, String... responses) throws IOException { + this.responses = responses; + this.expectedHeads = new CountDownLatch(expectedHeads); + serverSocket = new ServerSocket(0, 4, InetAddress.getByName("localhost")); + thread = new Thread(this::serve); + thread.setDaemon(true); + thread.start(); + } + + int port() { + return serverSocket.getLocalPort(); + } + + private void serve() { + while (!serverSocket.isClosed()) { + try (Socket socket = serverSocket.accept()) { + socket.setSoTimeout(5000); + InputStream in = socket.getInputStream(); + OutputStream out = socket.getOutputStream(); + int answered = 0; + String head; + while ((head = readHead(in)) != null) { + requestHeads.add(head); + expectedHeads.countDown(); + if (answered < responses.length) { + out.write(responses[answered++].getBytes(StandardCharsets.US_ASCII)); + out.flush(); + } + } + } catch (Exception ignored) { + // accept() throws once the socket is closed in tearDown, and a read times out when the client + // has nothing more to say. Either way there is nothing left to record on this connection. + } + } + } + + /** + * Reads one request head, i.e. up to and including the CRLFCRLF. Returns null at end of stream. Note + * that a TLS ClientHello (which is what a client sends once a tunnel really is established) contains no + * CRLFCRLF, so it is never mistaken for a request. + */ + private static String readHead(InputStream in) throws IOException { + StringBuilder head = new StringBuilder(); + int b3 = -1, b2 = -1, b1 = -1, b; + while ((b = in.read()) != -1) { + head.append((char) b); + if (b3 == '\r' && b2 == '\n' && b1 == '\r' && b == '\n') { + return head.toString(); + } + b3 = b2; + b2 = b1; + b1 = b; + } + return null; + } + + void awaitExpectedHeads() throws InterruptedException { + expectedHeads.await(6, TimeUnit.SECONDS); + } + + @Override + public void close() throws IOException { + serverSocket.close(); + thread.interrupt(); + } + } + + private static boolean isConnect(String head) { + return head.startsWith("CONNECT "); + } + + /** + * The origin's Authorization, as opposed to Proxy-Authorization which legitimately goes to the proxy. + */ + private static boolean carriesOriginAuthorization(String head) { + for (String line : head.split("\r\n")) { + String lower = line.toLowerCase(); + if (lower.startsWith("authorization:") && line.contains(ORIGIN_BASIC)) { + return true; + } + } + return false; + } + + /** + * The contract for a proxy that never established a tunnel: the only thing it may ever be sent is a + * CONNECT, and certainly not the origin's credentials. + */ + private void assertOnlyConnectsReachedTheProxy() { + assertFalse(proxy.requestHeads.isEmpty(), "the proxy should have received the CONNECT"); + for (String head : proxy.requestHeads) { + assertFalse(carriesOriginAuthorization(head), + "the origin's credentials must never be written to a socket on which no tunnel was " + + "established; the proxy received:\n" + head); + assertTrue(isConnect(head), + "a proxy that has not established a tunnel must only ever be sent CONNECTs, but got:\n" + head); + } + } + + private AsyncHttpClient clientWithOriginRealm(boolean followRedirect) { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setFollowRedirect(followRedirect) + .setRequestTimeout(4000) + // Non-preemptive, which is the default: the credentials are sent only in answer to a + // challenge. That is precisely what makes a 401 from the proxy enough to solicit them. + .setRealm(basicAuthRealm(ORIGIN_USER, ORIGIN_PASSWORD).build()) + .setProxyServer(proxyServer("localhost", proxy.port()).build()) + .build(); + return new DefaultAsyncHttpClient(config); + } + + /** + * The proxy rejects the CONNECT with a 401. The tunnel does not exist, so nothing that follows may carry + * the origin's credentials down that socket. + */ + @Test(timeOut = 30000) + public void proxyRejectingConnectWith401DoesNotHarvestOriginCredentials() throws Exception { + // Two heads are scripted because an UNFIXED client sends a second request - the origin GET, bearing + // the credentials the 401 solicited - and answering it keeps the reproduction fast. + proxy = new RecordingProxy(2, + "HTTP/1.1 401 Unauthorized\r\n" + + "WWW-Authenticate: Basic realm=\"origin\"\r\n" + + "Content-Length: 0\r\n" + + "\r\n", + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n" + + "\r\n"); + + Response response = null; + try (AsyncHttpClient client = clientWithOriginRealm(false)) { + try { + response = client.prepareGet("https://origin.example.com/secret").execute().get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + // What was written is the contract; whether the exchange completed is asserted separately below. + } + } finally { + proxy.awaitExpectedHeads(); + } + + assertOnlyConnectsReachedTheProxy(); + // The 401 is the proxy's answer to the CONNECT. It is surfaced to the caller as-is, and must never be + // mistaken for an origin challenge to answer. + assertEquals(response == null ? null : response.getStatusCode(), (Integer) 401, + "the proxy's rejection of the CONNECT must be surfaced to the caller"); + } + + /** + * Same, with a redirect instead of a challenge. The Location deliberately stays on the same origin, so + * the redirect is not cross-origin and the credential-stripping path does not mask the bug. + */ + @Test(timeOut = 30000) + public void proxyRejectingConnectWith302DoesNotHarvestOriginCredentials() throws Exception { + // An unfixed client follows the redirect on the same plaintext socket, is challenged there, and sends + // the credentials on a third request. + proxy = new RecordingProxy(3, + "HTTP/1.1 302 Found\r\n" + + "Location: https://origin.example.com/secret2\r\n" + + "Content-Length: 0\r\n" + + "\r\n", + "HTTP/1.1 401 Unauthorized\r\n" + + "WWW-Authenticate: Basic realm=\"origin\"\r\n" + + "Content-Length: 0\r\n" + + "\r\n", + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n" + + "\r\n"); + + try (AsyncHttpClient client = clientWithOriginRealm(true)) { + try { + client.prepareGet("https://origin.example.com/secret").execute().get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + // Whether the exchange completes is not the contract under test; what was written is. + } + } finally { + proxy.awaitExpectedHeads(); + } + + assertOnlyConnectsReachedTheProxy(); + } + + /** + * The regression guard for the other direction: once the proxy DOES establish the tunnel, the exchange + * must proceed exactly as before - the CONNECT carries Proxy-Authorization and the tunnelled request + * carries the origin's Authorization. A {@code ws://} target is used so the tunnelled hop is cleartext + * and can be inspected; the {@code https://} equivalent is covered end to end by HttpsProxyTest. + */ + @Test(timeOut = 30000) + public void establishedTunnelStillCarriesOriginAuthOnTheTunnelledRequest() throws Exception { + proxy = new RecordingProxy(2, "HTTP/1.1 200 Connection Established\r\n\r\n"); + + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setRequestTimeout(4000) + .setRealm(basicAuthRealm(ORIGIN_USER, ORIGIN_PASSWORD).setUsePreemptiveAuth(true).build()) + .setProxyServer(proxyServer("localhost", proxy.port()) + .setRealm(basicAuthRealm("proxy-user", "proxy-secret").setUsePreemptiveAuth(true)) + .build()) + .build(); + + try (AsyncHttpClient client = new DefaultAsyncHttpClient(config)) { + try { + client.prepareGet("ws://origin.example.com/socket") + .execute(new WebSocketUpgradeHandler.Builder().build()) + .get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + // The fake proxy never completes the WebSocket handshake; only what was written matters here. + } + } finally { + proxy.awaitExpectedHeads(); + } + + assertTrue(proxy.requestHeads.size() >= 2, + "the tunnelled request must be sent on the same socket after the 200, but got: " + proxy.requestHeads); + + String connect = proxy.requestHeads.get(0); + assertTrue(isConnect(connect), "the first request must be a CONNECT: " + connect); + assertTrue(connect.toLowerCase().contains("proxy-authorization: basic "), + "the CONNECT is addressed to the proxy and must carry its credentials:\n" + connect); + assertFalse(carriesOriginAuthorization(connect), + "the CONNECT travels in the clear and must NOT carry the origin's credentials:\n" + connect); + + String tunnelled = proxy.requestHeads.get(1); + assertFalse(isConnect(tunnelled), "the second request must be the tunnelled one: " + tunnelled); + assertTrue(carriesOriginAuthorization(tunnelled), + "once the tunnel is established the origin's credentials must be sent through it:\n" + tunnelled); + } +} From c961f1cc5065bddd54a4fdb68fcd8649f8017409 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Thu, 6 Aug 2026 22:21:54 +0000 Subject: [PATCH 22/32] Prepare 2.16.1 and correct the redirect-401 test comments Motivation: The advisories for this batch say the fixes are available in 2.16.1, but every pom still declared 2.16.0, so that statement could not become true. Separately, the cross-origin 401 case added with the config-realm fix carried two comments that contradicted each other. One said the streamed response framing keeps the connection alive so the authenticated retry reaches the handler; the other said the test server closes the connection on that retry, which is what the surrounding try/catch was there to absorb. Measured on JDK 11.0.31: sendResponseHeaders(401, 0) plus a closed body emits Transfer-Encoding: chunked with a terminating chunk and the connection is reused, so the first comment is right, the second is wrong, and the catch is dead - a probe that rethrew from it never fired. Modification: Bump every module and the scm tag to 2.16.1-SNAPSHOT, matching how 2.x prepares a release. Rewrite the framing comment to state the mechanism that was actually measured, including what the -1 alternative does (Content-Length: 0 and then a connection reset, which would strand the retry the test is watching for). Drop the dead try/catch and assert the 401 is returned instead, so a leak can no longer hide behind a request that failed for an unrelated reason. Result: The artifacts the advisories name can be built, and the test's comments describe the server behaviour that was observed rather than a plausible guess. RedirectCredentialSecurityTest: 19 tests, all green. --- bom/pom.xml | 2 +- client/pom.xml | 2 +- .../RedirectCredentialSecurityTest.java | 22 +++++++++---------- example/pom.xml | 2 +- extras/guava/pom.xml | 2 +- extras/jdeferred/pom.xml | 2 +- extras/pom.xml | 2 +- extras/registry/pom.xml | 2 +- extras/retrofit2/pom.xml | 2 +- extras/rxjava/pom.xml | 2 +- extras/rxjava2/pom.xml | 2 +- extras/simple/pom.xml | 2 +- extras/typesafeconfig/pom.xml | 2 +- netty-utils/pom.xml | 2 +- pom.xml | 4 ++-- 15 files changed, 25 insertions(+), 27 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index c135bd59d1..22db17210d 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ org.asynchttpclient async-http-client-project - 2.16.0 + 2.16.1-SNAPSHOT async-http-client-bom diff --git a/client/pom.xml b/client/pom.xml index a1f941250e..a3728c7578 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.16.0 + 2.16.1-SNAPSHOT 4.0.0 async-http-client diff --git a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java index 0f36aed467..79f3c4d3b5 100644 --- a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java +++ b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java @@ -27,7 +27,6 @@ import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; -import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -208,8 +207,10 @@ public static void startServers() throws Exception { authOn401Target.set(auth); } exchange.getResponseHeaders().add("WWW-Authenticate", "Basic realm=\"target\""); - // A body-carrying response keeps the connection alive, so the client's authenticated retry (the - // leak this test guards against) actually reaches this handler instead of dying on a closed socket. + // Streamed framing (length 0 plus a closed body), which this server emits as Transfer-Encoding: + // chunked with a terminating chunk, so the connection survives the exchange. sendResponseHeaders + // with -1 would send Content-Length: 0 and then reset the connection, and the authenticated retry + // this test is watching for - the leak - would die on a closed socket instead of reaching here. exchange.sendResponseHeaders(401, 0); exchange.getResponseBody().close(); exchange.close(); @@ -703,16 +704,13 @@ public void crossDomainRedirectTo401TargetDoesNotLeakConfigRealm() throws Except try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) { authOn401Target.set(null); - try { - client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b-401") - .execute() - .get(5, TimeUnit.SECONDS); - } catch (ExecutionException ignored) { - // Without the fix the 401 triggers an authenticated retry and the JDK test server closes the - // connection on it. What the request ends up returning is irrelevant here: the assertion below - // is the contract, and it must hold whether or not the exchange completed. - } + Response response = client.prepareGet("http://127.0.0.1:" + portA + "/redirect-to-b-401") + .execute() + .get(5, TimeUnit.SECONDS); + // The redirect is followed and the 401 is returned to the caller: the exchange completes either + // way, so a leak cannot hide behind a failed request. + assertEquals(response.getStatusCode(), 401); assertNull(authOn401Target.get(), "client-wide config Realm must not be sent to a cross-domain 401 target after redirect"); } diff --git a/example/pom.xml b/example/pom.xml index d88037ca0f..069db6cc33 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.16.0 + 2.16.1-SNAPSHOT 4.0.0 async-http-client-example diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index 8e5f62c720..5b31ac0fee 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.16.0 + 2.16.1-SNAPSHOT 4.0.0 async-http-client-extras-guava diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index 80fbd9bd44..847e2e6197 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,7 +18,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.0 + 2.16.1-SNAPSHOT async-http-client-extras-jdeferred Asynchronous Http Client JDeferred Extras diff --git a/extras/pom.xml b/extras/pom.xml index 006534c4ca..28bf661b9c 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.16.0 + 2.16.1-SNAPSHOT 4.0.0 async-http-client-extras-parent diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index 79a52a656e..2422eaa577 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.16.0 + 2.16.1-SNAPSHOT 4.0.0 async-http-client-extras-registry diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index c91daa9a54..092a7dcc0b 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.0 + 2.16.1-SNAPSHOT async-http-client-extras-retrofit2 diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index 8fb7cf99a5..670d99ebf9 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.0 + 2.16.1-SNAPSHOT async-http-client-extras-rxjava Asynchronous Http Client RxJava Extras diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index c1bce44e1c..ffa3a69bd8 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.0 + 2.16.1-SNAPSHOT async-http-client-extras-rxjava2 Asynchronous Http Client RxJava2 Extras diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index 5fd546ef95..9246101a36 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.0 + 2.16.1-SNAPSHOT async-http-client-extras-simple Asynchronous Http Simple Client diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index 5c5a8fd2fe..e0e57d7ad6 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.0 + 2.16.1-SNAPSHOT async-http-client-extras-typesafe-config diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index 41df40b3bb..35c201c302 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.16.0 + 2.16.1-SNAPSHOT 4.0.0 async-http-client-netty-utils diff --git a/pom.xml b/pom.xml index 7fc485cf09..1a009be616 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.asynchttpclient async-http-client-project - 2.16.0 + 2.16.1-SNAPSHOT pom Asynchronous Http Client Project @@ -34,7 +34,7 @@ scm:git:git@github.com:AsyncHttpClient/async-http-client.git scm:git:git@github.com:AsyncHttpClient/async-http-client.git https://github.com/AsyncHttpClient/async-http-client/tree/master - async-http-client-project-2.16.0 + async-http-client-project-2.16.1 From 443a86c29683163dd956377555efd40392e52751 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sat, 8 Aug 2026 08:35:26 +0000 Subject: [PATCH 23/32] Close the plaintext socket a proxy refused to turn into a tunnel Motivation: The previous commit stopped a rejected CONNECT being dispatched to the origin-request interceptors, which is what rebuilt the exchange as the ORIGIN request and put the origin's Authorization on a socket the proxy still reads in the clear. On 3.x that is enough, but only because of an unrelated commit 2.x never received: HttpHandler.abortAfterHandlingStatus there also aborts when the request was a CONNECT and the status is not 200, so the socket is closed. Without that clause the exchange runs on to handleChunk, where "close = abort || !future.isKeepAlive()" is decided by whatever the proxy chose to say. A proxy that sends an implicitly keep-alive rejection gets the still-plaintext socket offered to the pool - under a partition key that names the https origin, because that is the exchange's target. NettyRequestSender.sendRequestThroughProxy then takes a channel polled under such a key to be a tunnel that is already up and builds the ORIGIN request rather than a CONNECT. So the leak was not removed, only deferred by one request: [conn#1] CONNECT origin.example.com:443 [conn#1] GET /secret [conn#1] GET /secret authorization: Basic b3JpZ2luLXVzZXI6b3JpZ2luLXNlY3JldA== reproduced on the parent commit against a proxy that answers a bare 401, and likewise for 302, 500, 201 and a 401 NTLM challenge. The existing test did not catch it because it issued a single request and asserted one request head, which "socket closed" and "socket poisoned in the pool" satisfy equally. Modification: Backport the CONNECT clause into abortAfterHandlingStatus, threading the request method in from handleHttpResponse. A non-200 answer to a CONNECT now aborts, and abort closes. Independently, refuse to pool a channel whose last request was a CONNECT. A CONNECT the proxy DID accept is consumed by ConnectSuccessInterceptor, which takes over the channel and never lets the exchange reach the pool, so a CONNECT in hand at pool time means the tunnel was refused. This holds on the IOException-filter replay path too, which reaches the pool without going through abortAfterHandlingStatus at all. The drain callback samples that question when it is created, as it already does for keepAlive and partitionKey: both callers hand the drain over and then immediately move the future on to the next request, so by the time the last chunk arrives the future no longer describes the response being drained. Result: Each half was verified to stop the leak on its own: with the abort clause reverted and only the pool guard in place, the new test still passes. ConnectTunnelStateTest now issues two requests through one client against a proxy that numbers its connections, and requires the second request to arrive on a new one with no Authorization anywhere on the wire. On the parent commit it fails naming the leaked credential; the three existing cases are unchanged and green. HttpsProxyTest, ProxyTest, ProxyTunnellingTest, NTLMProxyTest, CustomHeaderProxyTest, SocksProxyCredentialLeakTest and ProxyUnauthorized407InterceptorTest all pass. --- .../netty/channel/ChannelManager.java | 39 +++- .../netty/handler/AsyncHttpClientHandler.java | 2 +- .../netty/handler/HttpHandler.java | 14 +- .../proxy/ConnectTunnelStateTest.java | 177 +++++++++++++++--- 4 files changed, 198 insertions(+), 34 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index caa3a56dd3..ed98c4fc6c 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -23,6 +23,7 @@ import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder; import io.netty.handler.codec.http.websocketx.WebSocket08FrameEncoder; import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator; @@ -48,6 +49,7 @@ import org.asynchttpclient.netty.handler.BoundedHttpContentDecompressor; import org.asynchttpclient.netty.handler.HttpHandler; import org.asynchttpclient.netty.handler.WebSocketHandler; +import org.asynchttpclient.netty.request.NettyRequest; import org.asynchttpclient.netty.request.NettyRequestSender; import org.asynchttpclient.netty.ssl.DefaultSslEngineFactory; import org.asynchttpclient.proxy.ProxyServer; @@ -263,6 +265,36 @@ private BoundedHttpContentDecompressor newHttpContentDecompressor() { return new BoundedHttpContentDecompressor(config.isKeepEncodingHeader(), config.getMaxDecompressedResponseSize()); } + /** + * A channel is pooled under a partition key that names the target origin, and + * {@code NettyRequestSender.sendRequestThroughProxy} takes a channel polled under such a key to be a + * tunnel that is already up: it sends the ORIGIN request on it rather than a CONNECT. A channel whose + * last request was a CONNECT is exactly the opposite, because a CONNECT the proxy DID accept is consumed + * by {@link org.asynchttpclient.netty.handler.intercept.ConnectSuccessInterceptor}, which takes over the + * channel and never lets the exchange reach the pool. So reaching here with a CONNECT in hand means the + * proxy refused the tunnel and the socket is still a plaintext hop to it - which must be closed, not + * pooled, or the next exchange for that origin sends the origin request, Authorization header included, + * down a hop the proxy is still reading in the clear. + */ + private static boolean isRefusedTunnel(NettyResponseFuture future) { + NettyRequest nettyRequest = future.getNettyRequest(); + return nettyRequest != null && nettyRequest.getHttpRequest().method() == HttpMethod.CONNECT; + } + + public final void tryToOfferChannelToPool(Channel channel, NettyResponseFuture future, boolean keepAlive, Object partitionKey) { + tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey, isRefusedTunnel(future)); + } + + private void tryToOfferChannelToPool(Channel channel, AsyncHandler asyncHandler, boolean keepAlive, Object partitionKey, + boolean refusedTunnel) { + if (refusedTunnel) { + LOGGER.debug("Not offering channel {} to the pool: the CONNECT on it was never established", channel); + closeChannel(channel); + return; + } + tryToOfferChannelToPool(channel, asyncHandler, keepAlive, partitionKey); + } + public final void tryToOfferChannelToPool(Channel channel, AsyncHandler asyncHandler, boolean keepAlive, Object partitionKey) { if (channel.isActive() && keepAlive) { LOGGER.debug("Adding key: {} for channel {}", partitionKey, channel); @@ -465,9 +497,14 @@ public void upgradePipelineForWebSockets(ChannelPipeline pipeline) { private OnLastHttpContentCallback newDrainCallback(final NettyResponseFuture future, final Channel channel, final boolean keepAlive, final Object partitionKey) { + // Sampled here rather than in call(), for the same reason keepAlive and partitionKey are: callers hand + // the drain over and immediately move the future on to the NEXT request, so by the time the last chunk + // arrives the future no longer describes the response being drained off this channel. + final boolean refusedTunnel = isRefusedTunnel(future); + return new OnLastHttpContentCallback(future) { public void call() { - tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey); + tryToOfferChannelToPool(channel, future.getAsyncHandler(), keepAlive, partitionKey, refusedTunnel); } }; } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java index ec158673f0..d109b17ede 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java @@ -234,7 +234,7 @@ void finishUpdate(NettyResponseFuture future, Channel channel, boolean close) if (close) { channelManager.closeChannel(channel); } else { - channelManager.tryToOfferChannelToPool(channel, future.getAsyncHandler(), true, future.getPartitionKey()); + channelManager.tryToOfferChannelToPool(channel, future, true, future.getPartitionKey()); } try { diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java index dddaeb34cb..447b903e5a 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java @@ -28,6 +28,7 @@ import org.asynchttpclient.netty.channel.ChannelManager; import org.asynchttpclient.netty.channel.Channels; import org.asynchttpclient.netty.request.NettyRequestSender; +import org.asynchttpclient.util.HttpConstants.ResponseStatusCodes; import java.io.IOException; import java.net.InetSocketAddress; @@ -40,8 +41,17 @@ public HttpHandler(AsyncHttpClientConfig config, ChannelManager channelManager, } private boolean abortAfterHandlingStatus(AsyncHandler handler, + HttpMethod httpMethod, NettyResponseStatus status) throws Exception { - return handler.onStatusReceived(status) == State.ABORT; + // For a non-200 response to a CONNECT the tunnel was NOT established: the socket is still a plaintext + // hop terminated by the proxy. It has to be either closed or reused by sending the CONNECT again, and + // closing is the simpler of the two. Letting the exchange run on would instead reach handleChunk, where + // close is only `!future.isKeepAlive()` - i.e. whatever the proxy chose to say - and the still-plaintext + // socket would be offered to the pool under a partition key that names the secured origin. The next + // exchange polls it and, believing a channel under that key to be a tunnel, sends the ORIGIN request + // down it, Authorization header included. + return handler.onStatusReceived(status) == State.ABORT + || httpMethod == HttpMethod.CONNECT && status.getStatusCode() != ResponseStatusCodes.OK_200; } private boolean abortAfterHandlingHeaders(AsyncHandler handler, @@ -75,7 +85,7 @@ private void handleHttpResponse(final HttpResponse response, final Channel chann HttpHeaders responseHeaders = response.headers(); if (!interceptors.exitAfterIntercept(channel, future, handler, response, status, responseHeaders)) { - boolean abort = abortAfterHandlingStatus(handler, status) || // + boolean abort = abortAfterHandlingStatus(handler, httpRequest.method(), status) || // abortAfterHandlingHeaders(handler, responseHeaders) || // abortAfterHandlingReactiveStreams(channel, future, handler); diff --git a/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java b/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java index 494cbe8d41..fe24db6440 100644 --- a/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java +++ b/client/src/test/java/org/asynchttpclient/proxy/ConnectTunnelStateTest.java @@ -33,11 +33,13 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static org.asynchttpclient.Dsl.basicAuthRealm; import static org.asynchttpclient.Dsl.proxyServer; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertTrue; /** @@ -69,15 +71,40 @@ public void tearDown() throws IOException { } } + /** + * One request head as the proxy saw it, together with the identity of the TCP connection it arrived on. + * The connection is the point: "the socket was closed" and "the socket was poisoned and left in the + * pool" are indistinguishable from a single request, and only the second one leaks. + */ + private static final class RecordedHead { + + final int connectionId; + final String head; + + RecordedHead(int connectionId, String head) { + this.connectionId = connectionId; + this.head = head; + } + + @Override + public String toString() { + return "[conn#" + connectionId + "] " + head; + } + } + /** * A minimal HTTP proxy that answers each request head it receives with the next scripted response and - * keeps the connection open, recording everything the client sends on it afterwards. + * keeps the connection open, recording everything the client sends on it afterwards. Each accepted + * connection is served on its own thread and numbered, so a client that opens a second connection is + * told apart from one that reuses the first. */ private static final class RecordingProxy implements Closeable { private final ServerSocket serverSocket; private final String[] responses; - private final List requestHeads = new CopyOnWriteArrayList<>(); + private final List requestHeads = new CopyOnWriteArrayList<>(); + private final List accepted = new CopyOnWriteArrayList<>(); + private final AtomicInteger connectionIds = new AtomicInteger(); /** * Counts down to the number of request heads the UNFIXED client would send, so a run that reproduces * the leak does not have to wait out a request timeout to record it. @@ -100,27 +127,41 @@ int port() { private void serve() { while (!serverSocket.isClosed()) { - try (Socket socket = serverSocket.accept()) { - socket.setSoTimeout(5000); - InputStream in = socket.getInputStream(); - OutputStream out = socket.getOutputStream(); - int answered = 0; - String head; - while ((head = readHead(in)) != null) { - requestHeads.add(head); - expectedHeads.countDown(); - if (answered < responses.length) { - out.write(responses[answered++].getBytes(StandardCharsets.US_ASCII)); - out.flush(); - } - } + try { + Socket socket = serverSocket.accept(); + accepted.add(socket); + int connectionId = connectionIds.incrementAndGet(); + Thread connectionThread = new Thread(() -> serveConnection(socket, connectionId)); + connectionThread.setDaemon(true); + connectionThread.start(); } catch (Exception ignored) { - // accept() throws once the socket is closed in tearDown, and a read times out when the client - // has nothing more to say. Either way there is nothing left to record on this connection. + // accept() throws once the server socket is closed in tearDown. + return; } } } + private void serveConnection(Socket socket, int connectionId) { + try (Socket toClose = socket) { + toClose.setSoTimeout(5000); + InputStream in = toClose.getInputStream(); + OutputStream out = toClose.getOutputStream(); + int answered = 0; + String head; + while ((head = readHead(in)) != null) { + requestHeads.add(new RecordedHead(connectionId, head)); + expectedHeads.countDown(); + if (answered < responses.length) { + out.write(responses[answered++].getBytes(StandardCharsets.US_ASCII)); + out.flush(); + } + } + } catch (Exception ignored) { + // A read times out or fails once the client has nothing more to say on this connection. Either + // way there is nothing left to record on it. + } + } + /** * Reads one request head, i.e. up to and including the CRLFCRLF. Returns null at end of stream. Note * that a TLS ClientHello (which is what a client sends once a tunnel really is established) contains no @@ -148,6 +189,13 @@ void awaitExpectedHeads() throws InterruptedException { @Override public void close() throws IOException { serverSocket.close(); + for (Socket socket : accepted) { + try { + socket.close(); + } catch (IOException ignored) { + // Already closed by the client or by the connection thread. + } + } thread.interrupt(); } } @@ -169,18 +217,35 @@ private static boolean carriesOriginAuthorization(String head) { return false; } + /** + * Any Authorization at all, whatever it carries. Nothing addressed to the origin has any business on a + * socket the proxy refused to turn into a tunnel. + */ + private static boolean carriesAnyAuthorization(String head) { + for (String line : head.split("\r\n")) { + if (line.toLowerCase().startsWith("authorization:")) { + return true; + } + } + return false; + } + /** * The contract for a proxy that never established a tunnel: the only thing it may ever be sent is a * CONNECT, and certainly not the origin's credentials. */ private void assertOnlyConnectsReachedTheProxy() { assertFalse(proxy.requestHeads.isEmpty(), "the proxy should have received the CONNECT"); - for (String head : proxy.requestHeads) { - assertFalse(carriesOriginAuthorization(head), + // The credential leak is checked across every head before the weaker "it was not even a CONNECT" + // check, so that a failure names the secret that escaped rather than the first symptom on the wire. + for (RecordedHead recorded : proxy.requestHeads) { + assertFalse(carriesAnyAuthorization(recorded.head), "the origin's credentials must never be written to a socket on which no tunnel was " - + "established; the proxy received:\n" + head); - assertTrue(isConnect(head), - "a proxy that has not established a tunnel must only ever be sent CONNECTs, but got:\n" + head); + + "established; the proxy received:\n" + recorded); + } + for (RecordedHead recorded : proxy.requestHeads) { + assertTrue(isConnect(recorded.head), + "a proxy that has not established a tunnel must only ever be sent CONNECTs, but got:\n" + recorded); } } @@ -298,16 +363,68 @@ public void establishedTunnelStillCarriesOriginAuthOnTheTunnelledRequest() throw assertTrue(proxy.requestHeads.size() >= 2, "the tunnelled request must be sent on the same socket after the 200, but got: " + proxy.requestHeads); - String connect = proxy.requestHeads.get(0); - assertTrue(isConnect(connect), "the first request must be a CONNECT: " + connect); - assertTrue(connect.toLowerCase().contains("proxy-authorization: basic "), + RecordedHead connect = proxy.requestHeads.get(0); + assertTrue(isConnect(connect.head), "the first request must be a CONNECT: " + connect); + assertTrue(connect.head.toLowerCase().contains("proxy-authorization: basic "), "the CONNECT is addressed to the proxy and must carry its credentials:\n" + connect); - assertFalse(carriesOriginAuthorization(connect), + assertFalse(carriesOriginAuthorization(connect.head), "the CONNECT travels in the clear and must NOT carry the origin's credentials:\n" + connect); - String tunnelled = proxy.requestHeads.get(1); - assertFalse(isConnect(tunnelled), "the second request must be the tunnelled one: " + tunnelled); - assertTrue(carriesOriginAuthorization(tunnelled), + RecordedHead tunnelled = proxy.requestHeads.get(1); + assertFalse(isConnect(tunnelled.head), "the second request must be the tunnelled one: " + tunnelled); + assertEquals(tunnelled.connectionId, connect.connectionId, + "the tunnelled request must go down the very socket the CONNECT established: " + proxy.requestHeads); + assertTrue(carriesOriginAuthorization(tunnelled.head), "once the tunnel is established the origin's credentials must be sent through it:\n" + tunnelled); } + + /** + * Two requests, one client. Asserting on a single request is not enough: "the socket was closed" and + * "the socket was left in the pool, still plaintext, under the https-origin partition key" produce the + * same one request head, and only the second one leaks. It leaks on the request AFTER the rejected + * CONNECT, because {@code NettyRequestSender.sendRequestThroughProxy} takes a channel polled under that + * key to be a tunnel that is already up and therefore sends the ORIGIN request on it rather than a new + * CONNECT - at which point the proxy's next 401 collects the origin's credentials. + * + *

So the second request must arrive on a NEW connection, identified server-side, and no Authorization + * may appear anywhere on the wire. + */ + @Test(timeOut = 30000) + public void proxyRejectingConnectDoesNotLeavePoisonedConnectionInThePool() throws Exception { + String unauthorized = "HTTP/1.1 401 Unauthorized\r\n" + + "WWW-Authenticate: Basic realm=\"origin\"\r\n" + + "Content-Length: 0\r\n" + + "\r\n"; + // Three heads are scripted because that is what the UNFIXED client sends: the CONNECT, then the origin + // GET down the pooled plaintext socket, then the same GET carrying the credentials the second 401 + // solicited. Answering all three keeps the reproduction fast and lets the leak be recorded in full. + proxy = new RecordingProxy(3, unauthorized, unauthorized, + "HTTP/1.1 200 OK\r\n" + + "Content-Length: 0\r\n" + + "\r\n"); + + try (AsyncHttpClient client = clientWithOriginRealm(false)) { + for (int i = 0; i < 2; i++) { + try { + client.prepareGet("https://origin.example.com/secret").execute().get(10, TimeUnit.SECONDS); + } catch (Exception ignored) { + // What was written is the contract; the per-request status is asserted by the tests above. + } + } + } finally { + // Give a client that would still leak every chance to send the third head before asserting. + proxy.awaitExpectedHeads(); + } + + assertOnlyConnectsReachedTheProxy(); + + assertTrue(proxy.requestHeads.size() >= 2, + "the second request must have reached the proxy, but it only saw: " + proxy.requestHeads); + RecordedHead first = proxy.requestHeads.get(0); + for (int i = 1; i < proxy.requestHeads.size(); i++) { + assertNotEquals(proxy.requestHeads.get(i).connectionId, first.connectionId, + "the socket on which the proxy REFUSED the CONNECT is not a tunnel and must never be pooled; " + + "the client kept using it: " + proxy.requestHeads); + } + } } From abaa8bd260beda3b85825ff95069ed11eaf4ca8d Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sat, 8 Aug 2026 08:38:35 +0000 Subject: [PATCH 24/32] Answer a 407 only when the proxy is the one that wrote it Motivation: The proxy-type guard added to exitAfterHandling407 asks what kind of proxy is configured, not who wrote the response in hand. Once a CONNECT succeeds the peer on the far end of the socket is the ORIGIN, while the configured proxy type is still HTTP - so the gate passes and the client answers the origin's 407 with the PROXY's credentials, inside the tunnel it just built for the origin. The NTLM branch is the worst of it: the Type-3 message is computed over a challenge the origin chose. A non-preemptive proxy realm is required, because the preemptive branch returns early and masks it. The same holds one exchange later, when the tunnelled channel comes from the pool: the tunnel was established by an earlier future, so a per-future flag alone does not see it. The target does - behind an HTTP proxy a secured or WebSocket target is only ever reached through a CONNECT. Separately, every one of these refusals ran AFTER isAndSetInProxyAuth, which is the exchange's one shot at proxy authentication. A 407 that is then declined burned the latch, so a later legitimate proxy challenge on the same exchange was refused with "auth was already performed" when no proxy auth had been performed at all. Modification: Answer a 407 only when the request it answers was addressed to the proxy: the CONNECT itself, or a request on a socket that is not a tunnel. Move that check, the existing proxy-type check and the null-realm check above isAndSetInProxyAuth, so declining a 407 no longer consumes the latch. 3.x already orders the first two that way. Result: Three new cases in ProxyUnauthorized407InterceptorTest, all failing before this commit: a 407 inside a tunnel this exchange established, a 407 inside a tunnel inherited from the pool, and the burnt latch. Two regression guards pass unchanged - the proxy's challenge to the CONNECT itself is still answered, and so is a 407 from an HTTP proxy on a plaintext hop, which is the flow the mechanism exists for. HttpsProxyTest, ProxyTest, ProxyTunnellingTest, NTLMProxyTest, CustomHeaderProxyTest, SocksProxyCredentialLeakTest, ConnectTunnelStateTest, BasicAuthTest and DigestAuthTest are green. --- .../ProxyUnauthorized407Interceptor.java | 25 ++++- .../ProxyUnauthorized407InterceptorTest.java | 104 +++++++++++++++++- 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java index ec2dfdeef1..d087b670bb 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java @@ -58,10 +58,10 @@ public boolean exitAfterHandling407(Channel channel, ProxyServer proxyServer, HttpRequest httpRequest) { - if (future.isAndSetInProxyAuth(true)) { - LOGGER.info("Can't handle 407 as auth was already performed"); - return false; - } + // The three questions below all decide whether the PROXY wrote this response, and none of them may be + // asked after isAndSetInProxyAuth: that latch is the exchange's one shot at proxy authentication, and + // burning it on a 407 we then decline makes a later, legitimate proxy challenge on the same exchange + // fail with "auth was already performed" when no proxy auth was ever performed at all. Realm proxyRealm = future.getProxyRealm(); @@ -79,6 +79,23 @@ public boolean exitAfterHandling407(Channel channel, return false; } + // Being an HTTP proxy only says the proxy CAN write a 407; it does not say it wrote this one. A CONNECT + // is addressed to the proxy, so its 407 is the proxy's. Anything else on a tunnelled socket reaches the + // ORIGIN, and answering the origin's 407 hands it the proxy's credentials. The tunnel may have been + // established by this exchange (tunnelEstablished) or inherited with a channel taken from the pool, in + // which case the flag is false but the target still implies one: behind an HTTP proxy a secured or + // WebSocket target is only ever reached through a CONNECT. + if (httpRequest.method() != HttpMethod.CONNECT + && (future.isTunnelEstablished() || request.getUri().isSecured() || request.getUri().isWebSocket())) { + LOGGER.debug("Can't handle 407: it arrived through a tunnel, so it came from the origin"); + return false; + } + + if (future.isAndSetInProxyAuth(true)) { + LOGGER.info("Can't handle 407 as auth was already performed"); + return false; + } + List proxyAuthHeaders = response.headers().getAll(PROXY_AUTHENTICATE); if (proxyAuthHeaders.isEmpty()) { diff --git a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java index 4df18bc7fc..5715f7b135 100644 --- a/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java +++ b/client/src/test/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407InterceptorTest.java @@ -110,8 +110,16 @@ private static Realm nonPreemptiveProxyRealm() { } private NettyResponseFuture newFuture(ProxyServer proxyServer, Realm proxyRealm) throws Exception { - Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); - HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/resource"); + return newFuture(proxyServer, proxyRealm, "http://origin.example.com/resource", HttpMethod.GET); + } + + private NettyResponseFuture newFuture(ProxyServer proxyServer, + Realm proxyRealm, + String url, + HttpMethod method) throws Exception { + Request request = new RequestBuilder(method.name()).setUrl(url).build(); + HttpRequest httpRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, + method == HttpMethod.CONNECT ? "origin.example.com:443" : "/resource"); AsyncHandler handler = new AsyncCompletionHandlerBase(); NettyResponseFuture future = new NettyResponseFuture<>(request, @@ -187,4 +195,96 @@ public void httpProxy407IsStillAnswered() throws Exception { assertTrue(future.getProxyRealm().isUsePreemptiveAuth(), "the retried request must carry the proxy credentials"); } + + /** + * Once a CONNECT has succeeded the peer on the far end of the socket is the ORIGIN, not the proxy, so a + * 407 arriving there was written by the origin. The proxy type is still HTTP, which is why asking what + * kind of proxy is configured does not answer the question that matters - who wrote this response. + */ + @Test + public void origin407InsideAnEstablishedTunnelIsNotAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer http = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(http, proxyRealm, "https://origin.example.com/resource", HttpMethod.GET); + future.setTunnelEstablished(true); + + boolean handled = interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + http, + future.getNettyRequest().getHttpRequest()); + + assertFalse(handled, + "a 407 seen inside an established tunnel was written by the origin and must not be answered " + + "with the proxy's credentials"); + assertFalse(future.getProxyRealm().isUsePreemptiveAuth(), + "the proxy realm must not be armed to send Proxy-Authorization to the origin"); + } + + /** + * The same request one exchange later. When the tunnelled channel comes from the pool the tunnel was + * established by an EARLIER future, so tunnelEstablished is false on this one - but the target still + * says what the socket must be: a secured or WebSocket target behind an HTTP proxy is only ever reached + * through a CONNECT, so anything that is not the CONNECT itself is talking to the origin. + */ + @Test + public void origin407OnATunnelInheritedFromThePoolIsNotAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer http = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(http, proxyRealm, "https://origin.example.com/resource", HttpMethod.GET); + + assertFalse(future.isTunnelEstablished(), "this exchange did not build the tunnel itself"); + assertFalse(interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + http, + future.getNettyRequest().getHttpRequest()), + "a secured target behind an HTTP proxy is reached through a tunnel, so a 407 on anything but " + + "the CONNECT came from the origin"); + } + + /** + * The regression guard for the flow the 407 mechanism exists for: the proxy challenges the CONNECT + * itself, which is addressed to it and travels in the clear. + */ + @Test + public void proxy407OnTheConnectIsStillAnswered() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer http = proxyServer("proxy.example.com", 8080).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(http, proxyRealm, "https://origin.example.com/resource", HttpMethod.CONNECT); + + assertTrue(interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + http, + future.getNettyRequest().getHttpRequest()), + "the proxy's challenge to the CONNECT must still be answered"); + assertTrue(future.getProxyRealm().isUsePreemptiveAuth(), + "the retried CONNECT must carry the proxy credentials"); + } + + /** + * A 407 this interceptor refuses to answer must not consume the exchange's one-shot proxy-auth latch + * either. Burning it means a later, legitimate proxy challenge on the same exchange is declined with + * "auth was already performed" when no proxy auth was ever performed at all. + */ + @Test + public void a407ThisInterceptorRefusesDoesNotBurnTheProxyAuthLatch() throws Exception { + Realm proxyRealm = nonPreemptiveProxyRealm(); + ProxyServer socks = proxyServer("proxy.example.com", 1080).setProxyType(ProxyType.SOCKS_V5).setRealm(proxyRealm).build(); + NettyResponseFuture future = newFuture(socks, proxyRealm); + + assertFalse(interceptor.exitAfterHandling407(channel, + future, + new407(), + future.getCurrentRequest(), + socks, + future.getNettyRequest().getHttpRequest())); + + assertFalse(future.isInProxyAuth(), + "declining a 407 must leave the proxy-auth latch untouched, since no proxy auth was performed"); + } } From 9411cf1db3f2220abe164851c3cda9969c2f1bfe Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sat, 8 Aug 2026 08:40:24 +0000 Subject: [PATCH 25/32] Release 2.16.1 as a concrete version, not a snapshot Motivation: The poms said 2.16.1-SNAPSHOT while scm/tag said async-http-client-project-2.16.1, which matches neither convention this branch has ever used. Every -SNAPSHOT version in 2.x history pairs with HEAD, and every concrete version with a concrete tag; the last four releases - 2.12.4, 2.14.5, 2.15.0 and 2.16.0, the branch base - took no snapshot step at all. Left as it stood, mvn deploy would publish 2.16.1-SNAPSHOT, and every advisory that names 2.16.1 as the fixed version would be pointing at something that was never released. Modification: Set the version to 2.16.1 across all fourteen poms - root, bom, client, example, netty-utils, the extras parent and its eight children - keeping the concrete scm tag. Result: git grep 2.16.1-SNAPSHOT and git grep 2.16.0 are both empty, and the full reactor builds at 2.16.1. --- bom/pom.xml | 2 +- client/pom.xml | 2 +- example/pom.xml | 2 +- extras/guava/pom.xml | 2 +- extras/jdeferred/pom.xml | 2 +- extras/pom.xml | 2 +- extras/registry/pom.xml | 2 +- extras/retrofit2/pom.xml | 2 +- extras/rxjava/pom.xml | 2 +- extras/rxjava2/pom.xml | 2 +- extras/simple/pom.xml | 2 +- extras/typesafeconfig/pom.xml | 2 +- netty-utils/pom.xml | 2 +- pom.xml | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bom/pom.xml b/bom/pom.xml index 22db17210d..ff4898eb88 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -5,7 +5,7 @@ org.asynchttpclient async-http-client-project - 2.16.1-SNAPSHOT + 2.16.1 async-http-client-bom diff --git a/client/pom.xml b/client/pom.xml index a3728c7578..b384b7fc5d 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.16.1-SNAPSHOT + 2.16.1 4.0.0 async-http-client diff --git a/example/pom.xml b/example/pom.xml index 069db6cc33..dd73d7e765 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.16.1-SNAPSHOT + 2.16.1 4.0.0 async-http-client-example diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index 5b31ac0fee..1f30fa4d4d 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.16.1-SNAPSHOT + 2.16.1 4.0.0 async-http-client-extras-guava diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index 847e2e6197..8df187d565 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,7 +18,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.1-SNAPSHOT + 2.16.1 async-http-client-extras-jdeferred Asynchronous Http Client JDeferred Extras diff --git a/extras/pom.xml b/extras/pom.xml index 28bf661b9c..7fda2624da 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.16.1-SNAPSHOT + 2.16.1 4.0.0 async-http-client-extras-parent diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index 2422eaa577..835e1e4874 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.16.1-SNAPSHOT + 2.16.1 4.0.0 async-http-client-extras-registry diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index 092a7dcc0b..86a4e9e672 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.1-SNAPSHOT + 2.16.1 async-http-client-extras-retrofit2 diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index 670d99ebf9..236a22c6c2 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.1-SNAPSHOT + 2.16.1 async-http-client-extras-rxjava Asynchronous Http Client RxJava Extras diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index ffa3a69bd8..fbb1b94a30 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.1-SNAPSHOT + 2.16.1 async-http-client-extras-rxjava2 Asynchronous Http Client RxJava2 Extras diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index 9246101a36..99c61c77bc 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,7 +3,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.1-SNAPSHOT + 2.16.1 async-http-client-extras-simple Asynchronous Http Simple Client diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index e0e57d7ad6..8c144715cf 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.16.1-SNAPSHOT + 2.16.1 async-http-client-extras-typesafe-config diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index 35c201c302..f2479fd9e4 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.16.1-SNAPSHOT + 2.16.1 4.0.0 async-http-client-netty-utils diff --git a/pom.xml b/pom.xml index 1a009be616..95b1ca6be6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.asynchttpclient async-http-client-project - 2.16.1-SNAPSHOT + 2.16.1 pom Asynchronous Http Client Project From 202281cf56133930c3a70b37fc686fb8e773aebd Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sat, 8 Aug 2026 08:41:54 +0000 Subject: [PATCH 26/32] Describe the 401 handler framing by what was measured Motivation: The comment on the /target-401 handler explained why the response is framed with a length of 0 by asserting that sendResponseHeaders(401, -1) "would send Content-Length: 0 and then reset the connection", stranding the authenticated retry. A controlled matrix over raw sockets and com.sun.net.httpserver on JDK 11.0.31 and JDK 25 says otherwise: -1 never resets anything, it is at worst a clean FIN, and with this handler otherwise unchanged (401, -1) reuses the connection on both JDKs, the retry does reach the handler and the leak is still observed. The variable that actually decides it is the getResponseBody().close(), not the -1. The comment therefore named both the wrong mechanism and the wrong cause, which would send the next reader chasing a reset that does not exist. Modification: Keep the accurate half - length 0 plus a closed body is emitted as chunked, so the connection survives - and replace the counterfactual half with the measured behaviour. Note as well that Unauthorized401Interceptor closes the channel and opens a fresh connection whenever it is not reusable, so a closed idle socket would not strand the retry in any case. Result: Comment only; RedirectCredentialSecurityTest is unchanged and its 19 cases pass. --- .../RedirectCredentialSecurityTest.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java index 79f3c4d3b5..6b52fbf94c 100644 --- a/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java +++ b/client/src/test/java/org/asynchttpclient/RedirectCredentialSecurityTest.java @@ -208,9 +208,15 @@ public static void startServers() throws Exception { } exchange.getResponseHeaders().add("WWW-Authenticate", "Basic realm=\"target\""); // Streamed framing (length 0 plus a closed body), which this server emits as Transfer-Encoding: - // chunked with a terminating chunk, so the connection survives the exchange. sendResponseHeaders - // with -1 would send Content-Length: 0 and then reset the connection, and the authenticated retry - // this test is watching for - the leak - would die on a closed socket instead of reaching here. + // chunked with a terminating chunk, so the connection survives the exchange. + // + // What decides that is the getResponseBody().close() below, not the 0. Measured against + // com.sun.net.httpserver on JDK 11.0.31 and JDK 25: with this handler otherwise unchanged, + // sendResponseHeaders(401, -1) reuses the connection too, the authenticated retry this test is + // watching for still reaches this handler, and the leak is still observed. Only dropping the body + // close strands it, and even then nothing is reset - -1 sends Content-Length: 0 and at worst a + // clean FIN. On the client side Unauthorized401Interceptor opens a fresh connection whenever the + // channel is not reusable, so a closed idle socket would not strand the retry either. exchange.sendResponseHeaders(401, 0); exchange.getResponseBody().close(); exchange.close(); From 1b701ada7ee10bf25bf3be98a58b35479c5df973 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sun, 9 Aug 2026 10:25:21 +0000 Subject: [PATCH 27/32] Trigger CI on the 2.x branch and on demand --- .github/workflows/maven.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index ef447f974e..4162e1ec8b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -5,9 +5,10 @@ name: Test PR on: pull_request: - branches: [ master, 2.14.5 ] + branches: [ 2.x ] push: - branches: [ 2.14.5 ] + branches: [ 2.x ] + workflow_dispatch: jobs: build: From 0d74898c4ac929d160da17663951b9c95ddc634e Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sun, 9 Aug 2026 10:40:11 +0000 Subject: [PATCH 28/32] Assert the Negotiate service name without initialising Kerberos Motivation: The test drove perConnectionAuthorizationHeader to read the service principal name back out of SpnegoEngine's log. Minting a token initialises the JVM's Kerberos configuration, and that initialisation is cached process-wide, so whenever this test ran before SpnegoEngineTest - which starts its own KDC and installs a krb5.conf in @BeforeClass - the later setup was ignored and two of its cases failed with "Cannot locate default realm". CI caught it; the local run had only ever happened to order them the other way. Modification: Extract the service-name choice into AuthenticatorUtils.negotiateHost and assert on that directly, so the test no longer reaches GSS. The production path is unchanged: perConnectionAuthorizationHeader calls the same method. Result: The same property is pinned - the origin realm's token targets the origin, never the proxy - deterministically, and SpnegoEngineTest is unaffected by test ordering. --- .../util/AuthenticatorUtils.java | 18 ++- .../PerConnectionAuthorizationHeaderTest.java | 119 ++++-------------- 2 files changed, 33 insertions(+), 104 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 757f87a508..fe3c694e20 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -161,6 +161,17 @@ public static String perRequestProxyAuthorizationHeader(Request request, Realm p return proxyAuthorization; } + /** + * The service the origin realm's Negotiate token is minted for. It is always the origin, even when a + * proxy is configured: building it against the proxy host produced a service ticket for the proxy's SPN, + * a confused deputy where the origin's credential is delivered to, and only usable by, the proxy, while + * origin authentication fails. The proxy realm has its own path, in + * perConnectionProxyAuthorizationHeader, and is unaffected. + */ + static String negotiateHost(Request request) { + return request.getVirtualHost() != null ? request.getVirtualHost() : request.getUri().getHost(); + } + public static String perConnectionAuthorizationHeader(Request request, ProxyServer proxyServer, Realm realm) { String authorizationHeader = null; @@ -172,12 +183,7 @@ public static String perConnectionAuthorizationHeader(Request request, ProxyServ break; case KERBEROS: case SPNEGO: - // The origin realm's Negotiate token must target the origin service even when a proxy is - // configured. Minting it against the proxy host produced a service ticket for the proxy's SPN: a - // confused deputy where the origin's credential is delivered to, and only usable by, the proxy, - // while origin authentication fails. The proxy realm has its own path, in - // perConnectionProxyAuthorizationHeader, and is unaffected. - String host = request.getVirtualHost() != null ? request.getVirtualHost() : request.getUri().getHost(); + String host = negotiateHost(request); try { authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance( diff --git a/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java b/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java index 3b68c83658..e1880c8240 100644 --- a/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java +++ b/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java @@ -13,130 +13,53 @@ */ package org.asynchttpclient.util; -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.AppenderBase; -import org.asynchttpclient.Realm; import org.asynchttpclient.Request; import org.asynchttpclient.RequestBuilder; -import org.asynchttpclient.proxy.ProxyServer; -import org.asynchttpclient.spnego.SpnegoEngine; -import org.slf4j.LoggerFactory; import org.testng.annotations.Test; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; - import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; /** - * The origin realm's Kerberos/SPNEGO token must be minted against the ORIGIN service, never against the - * proxy, even when a proxy is configured. Building it from {@code proxyServer.getHost()} produced a service - * ticket for the proxy's SPN: a confused deputy in which the origin's credential is delivered to, and only - * usable by, the proxy, while origin authentication fails. - * - *

Minting a real token needs a KDC, so the assertion is made on the service principal name the engine - * derives, which is logged before any GSS or network work happens. + * The origin realm's Negotiate token must be minted for the ORIGIN service, never the proxy's. Building it + * against the proxy host produced a service ticket for the proxy's SPN: a confused deputy where the origin's + * credential is delivered to, and only usable by, the proxy, while origin authentication fails. + *

+ * This asserts the service name that is chosen, rather than driving SpnegoEngine, deliberately. Minting a + * token initialises the JVM's Kerberos configuration, and that initialisation is cached process-wide, so a + * test that triggers it poisons {@code SpnegoEngineTest} - which starts its own KDC and installs a krb5.conf + * in {@code @BeforeClass} - whenever it happens to run first. */ public class PerConnectionAuthorizationHeaderTest { - private static final class CapturingAppender extends AppenderBase { - private final List messages = new CopyOnWriteArrayList<>(); - - @Override - protected void append(ILoggingEvent event) { - messages.add(event.getFormattedMessage()); - } - } - - /** - * Drives perConnectionAuthorizationHeader with a SPNEGO origin realm and returns the service principal - * name SpnegoEngine derived. Token generation itself fails without a KDC, which is irrelevant here: the - * SPN is chosen, and logged, before the first GSS call. - */ - private static String servicePrincipalNameFor(Request request, ProxyServer proxyServer) { - ch.qos.logback.classic.Logger engineLogger = - (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(SpnegoEngine.class); - Level previousLevel = engineLogger.getLevel(); - CapturingAppender appender = new CapturingAppender(); - appender.start(); - engineLogger.addAppender(appender); - engineLogger.setLevel(Level.DEBUG); - - try { - Realm realm = new Realm.Builder("principal", "password") - .setScheme(Realm.AuthScheme.SPNEGO) - .setUsePreemptiveAuth(true) - // Pin off DNS so the derived name is exactly the host we passed in. - .setUseCanonicalHostname(false) - .build(); - try { - AuthenticatorUtils.perConnectionAuthorizationHeader(request, proxyServer, realm); - } catch (Throwable expected) { - // No KDC in the build environment, so the handshake cannot complete. The SPN has already been - // derived and logged by this point. - } - - for (String message : appender.messages) { - if (message.startsWith("Service Principal Name is ")) { - return message.substring("Service Principal Name is ".length()); - } - } - throw new AssertionError("SpnegoEngine never derived a service principal name; captured: " + appender.messages); - - } finally { - engineLogger.detachAppender(appender); - engineLogger.setLevel(previousLevel); - appender.stop(); - } - } - @Test - public void spnegoTokenTargetsTheOriginEvenBehindAProxy() { + public void negotiateTokenTargetsTheOriginEvenBehindAProxy() { Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); - ProxyServer proxyServer = new ProxyServer.Builder("proxy.example.com", 8080).build(); - String spn = servicePrincipalNameFor(request, proxyServer); + String host = AuthenticatorUtils.negotiateHost(request); - assertEquals(spn, "HTTP@origin.example.com", + assertEquals(host, "origin.example.com", "the origin realm's Negotiate token must target the origin service"); - assertFalse(spn.contains("proxy.example.com"), - "the origin credential must not be minted for the proxy's SPN: " + spn); + assertFalse(host.contains("proxy"), + "the origin credential must not be minted for the proxy's SPN: " + host); } @Test - public void spnegoTokenPrefersTheVirtualHostBehindAProxy() { + public void negotiateTokenPrefersTheVirtualHost() { Request request = new RequestBuilder("GET") .setUrl("http://origin.example.com/resource") - .setVirtualHost("vhost.example.com") + .setVirtualHost("virtual.example.com") .build(); - ProxyServer proxyServer = new ProxyServer.Builder("proxy.example.com", 8080).build(); - - assertEquals(servicePrincipalNameFor(request, proxyServer), "HTTP@vhost.example.com"); - } - - @Test - public void spnegoTokenTargetsTheOriginWithoutAProxy() { - Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); - assertEquals(servicePrincipalNameFor(request, null), "HTTP@origin.example.com"); + assertEquals(AuthenticatorUtils.negotiateHost(request), "virtual.example.com", + "a configured virtual host names the service the request is really for"); } - /** - * The proxy realm keeps its own path and is unaffected: NTLM is minted per connection there, and the - * Kerberos/SPNEGO proxy token is built against the proxy host by ProxyUnauthorized407Interceptor. - */ @Test - public void proxyRealmStillGetsItsOwnPerConnectionHeader() { - Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); - Realm proxyRealm = new Realm.Builder("principal", "password") - .setScheme(Realm.AuthScheme.NTLM) - .setUsePreemptiveAuth(true) - .build(); + public void negotiateHostIgnoresThePort() { + Request request = new RequestBuilder("GET").setUrl("http://origin.example.com:8443/resource").build(); - String header = AuthenticatorUtils.perConnectionProxyAuthorizationHeader(request, proxyRealm); - assertTrue(header != null && header.startsWith("NTLM "), "expected an NTLM type-1 message but got: " + header); + assertEquals(AuthenticatorUtils.negotiateHost(request), "origin.example.com", + "the service principal name is built from the host alone"); } } From c8d639bf6ac341d377d610a93570bcd15565f1a6 Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sun, 9 Aug 2026 21:07:30 +0000 Subject: [PATCH 29/32] Fail a Digest challenge with no usable nonce instead of downgrading to Basic The scheme was chosen with isNonEmpty(nonce) ? DIGEST : BASIC, so any Digest challenge that produced no nonce became a Basic one and the client answered it with the password base64 encoded. Omitting the nonce, or sending an empty one, was enough. A peer cannot get this by offering Basic outright, because a Digest realm refuses a challenge that is not Digest, so the parse outcome was handing over what the honest path withholds. Take the scheme from the challenge itself. A challenge that announces itself as Digest stays Digest even when unreadable, and with no nonce it produces no Authorization header at all, so the exchange fails rather than downgrading. Both the target and the proxy paths are covered. --- .../main/java/org/asynchttpclient/Realm.java | 28 ++++++++++- .../java/org/asynchttpclient/RealmTest.java | 48 +++++++++++++++++++ .../spnego/SpnegoEngineTest.java | 43 +++++++++++++++++ .../PerConnectionAuthorizationHeaderTest.java | 26 +++------- 4 files changed, 123 insertions(+), 22 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index 35cd8b4b0a..83b6b6102e 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -416,11 +416,35 @@ private String parseRawQop(String rawQop) { return null; } + /** + * The scheme a parsed challenge authenticates with. + *

+ * A challenge that announces itself as Digest stays Digest even when its parameters do not parse. + * Deciding this from the presence of a nonce instead meant any Digest challenge we could not read + * became a Basic one, and answering it put the password on the wire in the clear, which is the single + * thing Digest exists to prevent. Omitting the nonce, or sending an empty one, was enough to trigger + * it. A Digest challenge with no nonce now produces no Authorization header at all, so the request + * fails rather than leaking. + */ + private static AuthScheme challengedScheme(String headerLine, String nonce) { + if (isNonEmpty(nonce)) { + return AuthScheme.DIGEST; + } + if (headerLine == null) { + return AuthScheme.BASIC; + } + int start = 0; + while (start < headerLine.length() && headerLine.charAt(start) == ' ') { + start++; + } + return headerLine.regionMatches(true, start, "Digest", 0, 6) ? AuthScheme.DIGEST : AuthScheme.BASIC; + } + public Builder parseWWWAuthenticateHeader(String headerLine) { setRealmName(match(headerLine, "realm")) .setNonce(match(headerLine, "nonce")) .setOpaque(match(headerLine, "opaque")) - .setScheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC); + .setScheme(challengedScheme(headerLine, nonce)); String algorithm = match(headerLine, "algorithm"); if (isNonEmpty(algorithm)) { setAlgorithm(algorithm); @@ -439,7 +463,7 @@ public Builder parseProxyAuthenticateHeader(String headerLine) { setRealmName(match(headerLine, "realm")) .setNonce(match(headerLine, "nonce")) .setOpaque(match(headerLine, "opaque")) - .setScheme(isNonEmpty(nonce) ? AuthScheme.DIGEST : AuthScheme.BASIC); + .setScheme(challengedScheme(headerLine, nonce)); String algorithm = match(headerLine, "algorithm"); if (isNonEmpty(algorithm)) { setAlgorithm(algorithm); diff --git a/client/src/test/java/org/asynchttpclient/RealmTest.java b/client/src/test/java/org/asynchttpclient/RealmTest.java index 5f17a9b6fd..7f278cbb9e 100644 --- a/client/src/test/java/org/asynchttpclient/RealmTest.java +++ b/client/src/test/java/org/asynchttpclient/RealmTest.java @@ -24,6 +24,54 @@ import static org.testng.Assert.assertEquals; public class RealmTest { + + /** + * A Digest challenge that yields no nonce must not become a Basic one. Answering it as Basic puts the + * password on the wire in the clear, and a server cannot obtain that by offering Basic outright, + * because a Digest realm refuses a challenge that is not Digest. Omitting the nonce, or sending it + * empty, was enough to get the password out of the client. + */ + @Test + public void aDigestChallengeWithNoNonceMustNotDowngradeToBasic() { + Realm realm = new Realm.Builder("user", "pass") + .parseWWWAuthenticateHeader("Digest realm=\"protected\"") + .build(); + + assertEquals(realm.getScheme(), Realm.AuthScheme.DIGEST, + "an unreadable Digest challenge must fail, not answer in cleartext"); + } + + @Test + public void aDigestChallengeWithAnEmptyNonceMustNotDowngradeToBasic() { + Realm realm = new Realm.Builder("user", "pass") + .parseWWWAuthenticateHeader("Digest realm=\"protected\", nonce=\"\"") + .build(); + + assertEquals(realm.getScheme(), Realm.AuthScheme.DIGEST); + } + + @Test + public void aProxyDigestChallengeWithNoNonceMustNotDowngradeToBasic() { + Realm realm = new Realm.Builder("user", "pass") + .parseProxyAuthenticateHeader("Digest realm=\"protected\"") + .build(); + + assertEquals(realm.getScheme(), Realm.AuthScheme.DIGEST, + "an unreadable proxy Digest challenge must fail, not answer in cleartext"); + } + + /** + * A genuine Basic challenge is still Basic: the fix must not turn every challenge into Digest. + */ + @Test + public void aBasicChallengeIsStillBasic() { + Realm realm = new Realm.Builder("user", "pass") + .parseWWWAuthenticateHeader("Basic realm=\"protected\"") + .build(); + + assertEquals(realm.getScheme(), Realm.AuthScheme.BASIC); + } + @Test public void testClone() { Realm orig = basicAuthRealm("user", "pass").setCharset(UTF_16) diff --git a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java index 92ff4a4d78..2370e6d855 100644 --- a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java +++ b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java @@ -3,6 +3,11 @@ import org.apache.commons.io.FileUtils; import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; import org.asynchttpclient.AbstractBasicTest; +import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.proxy.ProxyServer; +import org.asynchttpclient.util.AuthenticatorUtils; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -51,6 +56,10 @@ public static void startServers() throws Exception { kerbyServer.createPrincipal(alice, "alice"); kerbyServer.createPrincipal(bob, "bob"); + // Host based service principal for the origin, so a token can be obtained for HTTP@localhost and + // only for that host. negotiateTokenTargetsTheOriginEvenBehindAProxy relies on there being no such + // principal for the proxy host. + kerbyServer.createPrincipal("HTTP/localhost@service.ws.apache.org", "httppwd"); aliceKeytab = new File(basedir + File.separator + "target" + File.separator + "alice.keytab"); bobKeytab = new File(basedir + File.separator + "target" + File.separator + "bob.keytab"); @@ -77,6 +86,40 @@ public void testSpnegoGenerateTokenWithUsernamePassword() throws Exception { Assert.assertTrue(token.startsWith("YII")); } + /** + * The origin realm's Negotiate token must be minted for the origin service even when a proxy is + * configured. Building it against the proxy host produced a service ticket for the proxy's SPN: a + * confused deputy where the origin's credential is delivered to, and only usable by, the proxy, while + * origin authentication fails. + *

+ * This drives the real {@code perConnectionAuthorizationHeader} rather than the host-selection helper, + * so that restoring the proxy host in the product code makes it fail. The proxy host below has no + * service principal in the KDC, so a token can only be produced if the origin was chosen. It lives in + * this class because obtaining a token initialises the JVM's Kerberos configuration process-wide, and + * this is the class that already stands up a KDC and installs a krb5.conf for exactly that. + */ + @Test + public void negotiateTokenTargetsTheOriginEvenBehindAProxy() throws Exception { + Request request = new RequestBuilder("GET").setUrl("http://localhost:8080/resource").build(); + ProxyServer proxyServer = new ProxyServer.Builder("no-such-proxy.invalid", 3128).build(); + Realm realm = new Realm.Builder("alice", "alice") + .setScheme(Realm.AuthScheme.KERBEROS) + .setUsePreemptiveAuth(true) + // No explicit service principal name on purpose: setting one makes the SPN a constant and the + // host irrelevant, which is what made an earlier version of this test pass against both arms. + // Left unset, the name is HTTP@, so only the origin has a principal the KDC will issue for. + .setRealmName("service.ws.apache.org") + .setUseCanonicalHostname(false) + .setLoginContextName("alice") + .build(); + + String header = AuthenticatorUtils.perConnectionAuthorizationHeader(request, proxyServer, realm); + + Assert.assertNotNull(header, "no Negotiate header was produced for the origin"); + Assert.assertTrue(header.startsWith("Negotiate YII"), + "the origin credential must be minted for the origin service, not the proxy's SPN: " + header); + } + @Test(expectedExceptions = SpnegoEngineException.class) public void testSpnegoGenerateTokenWithUsernamePasswordFail() throws Exception { SpnegoEngine spnegoEngine = new SpnegoEngine("alice", diff --git a/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java b/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java index e1880c8240..7c6e0c61e6 100644 --- a/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java +++ b/client/src/test/java/org/asynchttpclient/util/PerConnectionAuthorizationHeaderTest.java @@ -18,32 +18,18 @@ import org.testng.annotations.Test; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; /** - * The origin realm's Negotiate token must be minted for the ORIGIN service, never the proxy's. Building it - * against the proxy host produced a service ticket for the proxy's SPN: a confused deputy where the origin's - * credential is delivered to, and only usable by, the proxy, while origin authentication fails. + * The contract of the host that names the service a Negotiate token is minted for. *

- * This asserts the service name that is chosen, rather than driving SpnegoEngine, deliberately. Minting a - * token initialises the JVM's Kerberos configuration, and that initialisation is cached process-wide, so a - * test that triggers it poisons {@code SpnegoEngineTest} - which starts its own KDC and installs a krb5.conf - * in {@code @BeforeClass} - whenever it happens to run first. + * That the origin is chosen rather than the proxy is asserted by + * {@code SpnegoEngineTest.negotiateTokenTargetsTheOriginEvenBehindAProxy}, which drives the real + * {@code perConnectionAuthorizationHeader} against a KDC and so fails if the product code regresses. These + * are the cheap unit-level companions to it, covering the two inputs that test does not vary. They mint no + * token and touch no Kerberos configuration. */ public class PerConnectionAuthorizationHeaderTest { - @Test - public void negotiateTokenTargetsTheOriginEvenBehindAProxy() { - Request request = new RequestBuilder("GET").setUrl("http://origin.example.com/resource").build(); - - String host = AuthenticatorUtils.negotiateHost(request); - - assertEquals(host, "origin.example.com", - "the origin realm's Negotiate token must target the origin service"); - assertFalse(host.contains("proxy"), - "the origin credential must not be minted for the proxy's SPN: " + host); - } - @Test public void negotiateTokenPrefersTheVirtualHost() { Request request = new RequestBuilder("GET") From bbc31aed3b044f9f7a126cf689a8c8d7ad2ae1cb Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sun, 9 Aug 2026 21:10:21 +0000 Subject: [PATCH 30/32] Move the target and the proxy together when a request is replayed Replaying onto a different host is what a ResponseFilter failover does, but only the current request moved. The target request stayed on the first origin, so the socket opened to the second host was filed in the connection pool under the first host's key, and a later request to the first host was served over it carrying that host's credentials. The credentials computed for the first origin also followed the replay to a host they were not meant for. Move the target, and with it the proxy, which is the other half of the pool key. Sample the old key first, since the channel being drained is still connected to the old origin, and drop the previous origin's realm on a cross-base or downgrading replay. The existing replay tests all replay to the same host, so none of this was covered. --- .../netty/NettyResponseFuture.java | 13 +- .../netty/request/NettyRequestSender.java | 22 +- .../filter/CrossHostReplayTest.java | 242 ++++++++++++++++++ 3 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 client/src/test/java/org/asynchttpclient/filter/CrossHostReplayTest.java diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java index 68edc75bc5..4174bffd9e 100755 --- a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java +++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java @@ -81,7 +81,9 @@ public final class NettyResponseFuture implements ListenableFuture { private final long start = unpreciseMillisTime(); private final ChannelPoolPartitioning connectionPoolPartitioning; private final ConnectionSemaphore connectionSemaphore; - private final ProxyServer proxyServer; + // Not final: a filter replay can retarget this future at a different origin, reached through a + // different proxy or none at all. getPartitionKey() reads it, so it has to move with the target. + private ProxyServer proxyServer; private final int maxRetry; private final CompletableFuture future = new CompletableFuture<>(); public Throwable pendingException; @@ -308,6 +310,15 @@ public Uri getUri() { return targetRequest.getUri(); } + /** + * Points this future at the proxy serving its current target. Only a replay onto a different origin + * needs this: leaving the previous origin's proxy in place leaves the connection pool partition key + * naming a route the future no longer takes. + */ + public void setProxyServer(ProxyServer proxyServer) { + this.proxyServer = proxyServer; + } + public ProxyServer getProxyServer() { return proxyServer; } diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index c327e14c90..d7348ea5b3 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -629,7 +629,27 @@ public void replayRequest(final NettyResponseFuture future, FilterContext fc, return; } - channelManager.drainChannelAndOffer(channel, future); + // A replay may target a different origin than the one this future was built for; failover onto a + // second host is the documented use of a ResponseFilter. Everything keyed off the future's TARGET has + // to move with it or it keeps describing the previous origin: the connection pool partition key, and + // the credentials, which must not follow the request to a host they were not meant for. + Uri previousUri = future.getTargetRequest().getUri(); + Uri newUri = newRequest.getUri(); + boolean sameBase = previousUri.isSameBase(newUri); + boolean schemeDowngrade = previousUri.isSecured() && !newUri.isSecured(); + // Sampled while the future still describes the old origin: that is what the drained channel is + // connected to, so that is the key it has to be filed under. + Object initialPartitionKey = future.getPartitionKey(); + + if (!sameBase || schemeDowngrade) { + future.setRealm(newRequest.getRealm()); + future.setProxyRealm(null); + } + // The proxy is the other half of the pool key and the replay may resolve to a different one, or none. + future.setProxyServer(getProxyServer(config, newRequest)); + future.setTargetRequest(newRequest); + + channelManager.drainChannelAndOffer(channel, future, future.isKeepAlive(), initialPartitionKey); sendNextRequest(newRequest, future); } diff --git a/client/src/test/java/org/asynchttpclient/filter/CrossHostReplayTest.java b/client/src/test/java/org/asynchttpclient/filter/CrossHostReplayTest.java new file mode 100644 index 0000000000..50e67d70d3 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/filter/CrossHostReplayTest.java @@ -0,0 +1,242 @@ +/* + * Copyright (c) 2026 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.filter; + +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.Dsl; +import org.asynchttpclient.Realm; +import org.asynchttpclient.RequestBuilder; +import org.asynchttpclient.proxy.ProxyServer; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * A {@link ResponseFilter} that replays onto a different host is the documented way to do + * failover. The request the future was built for and the request it ends up sending then disagree, and + * anything derived from the target has to move with it. + *

+ * The existing replay tests all replay to the same host, so nothing here was covered: the future kept + * describing the first origin, which sent that origin's credentials to the second one and filed the second + * one's socket in the connection pool under the first one's key. + */ +public class CrossHostReplayTest { + + private static final String SECRET = "s3cr3t-for-A"; + + /** + * A tiny origin that records the request line and Authorization header of everything it receives. + */ + private static final class Recorder implements AutoCloseable { + final ServerSocket socket = new ServerSocket(0); + final List seen = new CopyOnWriteArrayList<>(); + private final int status; + + Recorder(int status) throws IOException { + this.status = status; + Thread t = new Thread(this::serve); + t.setDaemon(true); + t.start(); + } + + int port() { + return socket.getLocalPort(); + } + + private void serve() { + while (!socket.isClosed()) { + final Socket s; + try { + s = socket.accept(); + } catch (IOException e) { + return; + } + // One thread per connection, and the connection is kept open across requests. Without + // keep-alive nothing is ever offered to the connection pool, and a test about pool keys + // would pass no matter what the product code did. + Thread worker = new Thread(() -> handle(s)); + worker.setDaemon(true); + worker.start(); + } + } + + private void handle(Socket s) { + try (Socket conn = s) { + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), ISO_8859_1)); + while (true) { + String requestLine = in.readLine(); + if (requestLine == null) { + return; + } + String auth = null; + String line; + while ((line = in.readLine()) != null && !line.isEmpty()) { + if (line.toLowerCase().startsWith("authorization:")) { + auth = line.substring("authorization:".length()).trim(); + } + } + seen.add(requestLine.split(" ")[1] + " auth=" + (auth == null ? "" : auth)); + conn.getOutputStream().write( + ("HTTP/1.1 " + status + " X\r\nContent-Length: 0\r\n\r\n").getBytes(ISO_8859_1)); + conn.getOutputStream().flush(); + } + } catch (IOException ignored) { + // client hung up + } + } + + @Override + public void close() throws IOException { + socket.close(); + } + } + + /** + * Replays onto {@code url} when the origin answers 503. ResponseFilter.filter is generic, so this + * cannot be a lambda. + */ + private static ResponseFilter failoverTo(String url) { + return new ResponseFilter() { + @Override + public FilterContext filter(FilterContext ctx) { + if (ctx.getResponseStatus() != null && ctx.getResponseStatus().getStatusCode() == 503) { + return new FilterContext.FilterContextBuilder<>(ctx) + .request(new RequestBuilder("GET").setUrl(url).build()) + .replayRequest(true) + .build(); + } + return ctx; + } + }; + } + + /** + * Replays onto {@code url} through the proxy on {@code proxyPort} when the origin answers 503. + */ + private static ResponseFilter failoverVia(String url, int proxyPort) { + return new ResponseFilter() { + @Override + public FilterContext filter(FilterContext ctx) { + if (ctx.getResponseStatus() != null && ctx.getResponseStatus().getStatusCode() == 503) { + return new FilterContext.FilterContextBuilder<>(ctx) + .request(new RequestBuilder("GET").setUrl(url) + .setProxyServer(new ProxyServer.Builder("127.0.0.1", proxyPort).build()) + .build()) + .replayRequest(true) + .build(); + } + return ctx; + } + }; + } + + private static Realm realmForA() { + return Dsl.basicAuthRealm("alice", SECRET).setUsePreemptiveAuth(true).build(); + } + + /** + * The credentials configured for the first origin must not be sent to the host the request is replayed + * onto. The replay carries no realm of its own, and the previous origin's must not be reused for it. + */ + @Test + public void replayToADifferentHostDoesNotCarryTheFirstHostsCredentials() throws Exception { + try (Recorder a = new Recorder(503); Recorder b = new Recorder(200)) { + String urlB = "http://127.0.0.1:" + b.port() + "/failover"; + ResponseFilter failover = failoverTo(urlB); + + try (AsyncHttpClient client = Dsl.asyncHttpClient( + Dsl.config().setMaxRequestRetry(0).addResponseFilter(failover))) { + client.prepareGet("http://127.0.0.1:" + a.port() + "/probe") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + } + + assertEquals(b.seen.size(), 1, "the failover host should have been called exactly once: " + b.seen); + assertTrue(b.seen.get(0).endsWith("auth="), + "the first host's credentials must not follow the replay: " + b.seen.get(0)); + } + } + + /** + * The socket opened for the replay target must be filed in the connection pool under that target, not + * under the origin the future was created for. Filed under the wrong key, a later request the + * application addresses to the first host is served over the connection to the second one, and the + * first host's credentials go there with it. + */ + @Test + public void replayToADifferentHostDoesNotFileTheSocketUnderTheFirstHostsKey() throws Exception { + try (Recorder a = new Recorder(503); Recorder b = new Recorder(200)) { + String urlB = "http://127.0.0.1:" + b.port() + "/failover"; + ResponseFilter failover = failoverTo(urlB); + + try (AsyncHttpClient client = Dsl.asyncHttpClient( + Dsl.config().setMaxRequestRetry(0).addResponseFilter(failover))) { + client.prepareGet("http://127.0.0.1:" + a.port() + "/probe") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + + // Addressed to A. If the replay left B's socket pooled under A's key, this is served by B. + client.prepareGet("http://127.0.0.1:" + a.port() + "/admin") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + } + + assertTrue(b.seen.stream().noneMatch(r -> r.startsWith("/admin")), + "a request addressed to the first host was served by the failover host: " + b.seen); + assertTrue(b.seen.stream().noneMatch(r -> r.contains(SECRET) || r.contains("YWxpY2U6")), + "the first host's credentials reached the failover host: " + b.seen); + } + } + + /** + * A replay that routes through a proxy must file its socket under a key naming that proxy. The proxy is + * half of the connection pool key and is memoized alongside the target, so moving the target while the + * proxy still names the previous route files a proxied connection under the direct key for the new + * host. The next request the application sends directly to that host then draws a socket that runs + * through the proxy, and its credentials go to the proxy. + */ + @Test + public void replayThroughAProxyDoesNotFileTheProxiedSocketAsDirect() throws Exception { + try (Recorder a = new Recorder(503); Recorder proxy = new Recorder(200); Recorder b = new Recorder(200)) { + String urlB = "http://127.0.0.1:" + b.port() + "/failover"; + + try (AsyncHttpClient client = Dsl.asyncHttpClient( + Dsl.config().setMaxRequestRetry(0).addResponseFilter(failoverVia(urlB, proxy.port())))) { + // Direct to A, which fails over onto B through the proxy. + client.prepareGet("http://127.0.0.1:" + a.port() + "/probe") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + + // Addressed directly to B, no proxy. Must not reuse the connection that runs through one. + client.prepareGet("http://127.0.0.1:" + b.port() + "/admin") + .setRealm(realmForA()) + .execute().get(30, TimeUnit.SECONDS); + } + + assertTrue(proxy.seen.stream().noneMatch(r -> r.contains("/admin")), + "a request addressed directly to the origin was served over the pooled proxy connection: " + + proxy.seen); + } + } +} From 73813babf51231ee79f7da4f07b777f19f48d34d Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sun, 9 Aug 2026 22:45:22 +0000 Subject: [PATCH 31/32] Keep a connection authenticated by one principal away from another NTLM and Negotiate authenticate the connection, not the request, so a socket one principal authenticated could be handed to another and the server would serve it as the first principal. Fold the principal into the connection pool key for those schemes, at every site that offers or polls, including the redirect and replay paths that hand a drained channel back. Any site left unscoped would either reopen the hole or, paired with a scoped one, lose reuse and hold permits for connections nothing can draw. The semaphore stays keyed per host, and Basic and Digest are untouched: they authenticate each request. --- .../netty/channel/ChannelManager.java | 11 +- .../channel/PrincipalScopedPartitionKey.java | 92 +++++++++++ .../netty/handler/AsyncHttpClientHandler.java | 6 +- .../intercept/Redirect30xInterceptor.java | 6 +- .../netty/request/NettyRequestSender.java | 13 +- .../channel/PooledConnectionIdentityTest.java | 149 ++++++++++++++++++ .../PrincipalScopedPartitionKeyTest.java | 89 +++++++++++ 7 files changed, 361 insertions(+), 5 deletions(-) create mode 100644 client/src/main/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKey.java create mode 100644 client/src/test/java/org/asynchttpclient/netty/channel/PooledConnectionIdentityTest.java create mode 100644 client/src/test/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKeyTest.java diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index ed98c4fc6c..35747456b8 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -320,6 +320,14 @@ public Channel poll(Uri uri, String virtualHost, ProxyServer proxy, ChannelPoolP Object partitionKey = connectionPoolPartitioning.getPartitionKey(uri, virtualHost, proxy); return channelPool.poll(partitionKey); } + /** + * Polls with a partition key the caller already derived, so the caller can scope it by the + * authenticated identity. See {@link PrincipalScopedPartitionKey}. + */ + public Channel poll(Object partitionKey) { + return channelPool.poll(partitionKey); + } + public void removeAll(Channel connection) { channelPool.removeAll(connection); @@ -510,7 +518,8 @@ public void call() { } public void drainChannelAndOffer(Channel channel, NettyResponseFuture future) { - drainChannelAndOffer(channel, future, future.isKeepAlive(), future.getPartitionKey()); + drainChannelAndOffer(channel, future, future.isKeepAlive(), + PrincipalScopedPartitionKey.scope(future.getPartitionKey(), future.getRealm())); } public void drainChannelAndOffer(Channel channel, NettyResponseFuture future, boolean keepAlive, Object partitionKey) { diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKey.java b/client/src/main/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKey.java new file mode 100644 index 0000000000..0f4e3277fb --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKey.java @@ -0,0 +1,92 @@ +/* + * 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.channel; + +import org.asynchttpclient.Realm; + +import java.util.Objects; + +/** + * Channel-pool partition key for schemes that authenticate the connection rather than the request. + * + *

NTLM and Negotiate complete a handshake once and the server then treats every later request arriving + * on that socket as coming from the identity that authenticated it. The regular partition key describes + * only where the connection goes, so a socket one principal authenticated could be handed to a request + * belonging to another, and the server would serve it as the first principal. Nothing on the wire shows the + * identity changed, because the second request carries no authentication headers of its own. + * + *

Folding the principal into the key keeps those connections separated. Basic and Digest need no such + * thing: they authenticate each request and their credentials travel with it. + * + *

If a site that offers a connection to the pool and a site that polls for one ever disagree about + * whether to scope, the poll simply misses and a new connection is opened. That costs reuse, never + * correctness, which is the right way round for this to fail. + */ +public final class PrincipalScopedPartitionKey { + + private final Object baseKey; + private final Realm.AuthScheme scheme; + private final String principal; + + private PrincipalScopedPartitionKey(Object baseKey, Realm.AuthScheme scheme, String principal) { + this.baseKey = baseKey; + this.scheme = scheme; + this.principal = principal; + } + + /** + * Wraps {@code baseKey} with the authenticated identity when {@code realm} uses a scheme that + * authenticates the connection, and returns {@code baseKey} unchanged otherwise. Every site that + * derives a pool key must apply this the same way. + */ + public static Object scope(Object baseKey, Realm realm) { + if (realm == null || realm.getPrincipal() == null || !authenticatesTheConnection(realm.getScheme())) { + return baseKey; + } + return new PrincipalScopedPartitionKey(baseKey, realm.getScheme(), realm.getPrincipal()); + } + + private static boolean authenticatesTheConnection(Realm.AuthScheme scheme) { + return scheme == Realm.AuthScheme.NTLM + || scheme == Realm.AuthScheme.KERBEROS + || scheme == Realm.AuthScheme.SPNEGO; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PrincipalScopedPartitionKey that = (PrincipalScopedPartitionKey) o; + return Objects.equals(baseKey, that.baseKey) + && scheme == that.scheme + && Objects.equals(principal, that.principal); + } + + @Override + public int hashCode() { + return 31 * (31 * Objects.hashCode(baseKey) + Objects.hashCode(scheme)) + Objects.hashCode(principal); + } + + @Override + public String toString() { + // The principal is a username, not a secret, and the existing keys print their host and proxy. + return "PrincipalScopedPartitionKey(baseKey=" + baseKey + ", scheme=" + scheme + ", principal=" + principal + ')'; + } +} diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java index d109b17ede..9fa7ee091f 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java @@ -29,6 +29,7 @@ import org.asynchttpclient.netty.OnLastHttpContentCallback; import org.asynchttpclient.netty.channel.ChannelManager; import org.asynchttpclient.netty.channel.Channels; +import org.asynchttpclient.netty.channel.PrincipalScopedPartitionKey; import org.asynchttpclient.netty.future.StackTraceInspector; import org.asynchttpclient.netty.handler.intercept.Interceptors; import org.asynchttpclient.netty.request.NettyRequestSender; @@ -234,7 +235,10 @@ void finishUpdate(NettyResponseFuture future, Channel channel, boolean close) if (close) { channelManager.closeChannel(channel); } else { - channelManager.tryToOfferChannelToPool(channel, future, true, future.getPartitionKey()); + // A connection authenticated by NTLM or Negotiate is filed under the identity that + // authenticated it, so no other principal can draw it. + channelManager.tryToOfferChannelToPool(channel, future, true, + PrincipalScopedPartitionKey.scope(future.getPartitionKey(), future.getRealm())); } try { diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java index b2696115ff..ce292128ad 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java @@ -27,6 +27,7 @@ import org.asynchttpclient.handler.MaxRedirectException; import org.asynchttpclient.netty.NettyResponseFuture; import org.asynchttpclient.netty.channel.ChannelManager; +import org.asynchttpclient.netty.channel.PrincipalScopedPartitionKey; import org.asynchttpclient.netty.request.NettyRequestSender; import org.asynchttpclient.uri.Uri; import org.slf4j.Logger; @@ -147,7 +148,10 @@ else if (isNonEmpty(request.getBodyParts())) { // in case of a redirect from HTTP to HTTPS, future // attributes might change final boolean initialConnectionKeepAlive = future.isKeepAlive(); - final Object initialPartitionKey = future.getPartitionKey(); + // Scoped like every other offer: an NTLM or Negotiate connection must stay with the identity + // that authenticated it, and a same-host redirect during NTLM is the ordinary case. + final Object initialPartitionKey = PrincipalScopedPartitionKey.scope( + future.getPartitionKey(), future.getRealm()); CookieStore cookieStore = config.getCookieStore(); if (cookieStore != null) { diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java index d7348ea5b3..0adcf72759 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -35,6 +35,7 @@ import org.asynchttpclient.netty.OnLastHttpContentCallback; import org.asynchttpclient.netty.SimpleFutureListener; import org.asynchttpclient.netty.channel.*; +import org.asynchttpclient.netty.channel.PrincipalScopedPartitionKey; import org.asynchttpclient.netty.handler.StreamedResponsePublisher; import org.asynchttpclient.netty.timeout.TimeoutsHolder; import org.asynchttpclient.proxy.ProxyServer; @@ -604,7 +605,14 @@ private Channel pollPooledChannel(Request request, ProxyServer proxy, AsyncHandl Uri uri = request.getUri(); String virtualHost = request.getVirtualHost(); - final Channel channel = channelManager.poll(uri, virtualHost, proxy, request.getChannelPoolPartitioning()); + // Scope the pool lookup by the authenticated identity: NTLM and Negotiate authenticate the socket, + // not the request, so a connection one principal completed the handshake on must not be handed to + // another. A disagreement with the offer side costs a pool miss, never a wrong reuse. + // Resolved as newNettyRequestAndResponseFuture resolves it, so the poll and the offer agree. + Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm(); + Object partitionKey = PrincipalScopedPartitionKey.scope( + request.getChannelPoolPartitioning().getPartitionKey(uri, virtualHost, proxy), realm); + final Channel channel = channelManager.poll(partitionKey); if (channel != null) { LOGGER.debug("Using pooled Channel '{}' for '{}' to '{}'", channel, request.getMethod(), uri); @@ -639,7 +647,8 @@ public void replayRequest(final NettyResponseFuture future, FilterContext fc, boolean schemeDowngrade = previousUri.isSecured() && !newUri.isSecured(); // Sampled while the future still describes the old origin: that is what the drained channel is // connected to, so that is the key it has to be filed under. - Object initialPartitionKey = future.getPartitionKey(); + Object initialPartitionKey = PrincipalScopedPartitionKey.scope( + future.getPartitionKey(), future.getRealm()); if (!sameBase || schemeDowngrade) { future.setRealm(newRequest.getRealm()); diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/PooledConnectionIdentityTest.java b/client/src/test/java/org/asynchttpclient/netty/channel/PooledConnectionIdentityTest.java new file mode 100644 index 0000000000..bbdd6c33ca --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/channel/PooledConnectionIdentityTest.java @@ -0,0 +1,149 @@ +/* + * 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.channel; + +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.Dsl; +import org.asynchttpclient.Realm; +import org.testng.annotations.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static org.testng.Assert.assertEquals; + +/** + * A connection authenticated by NTLM or Negotiate belongs to the identity that authenticated it, because + * those schemes authenticate the socket rather than the request. Handing it to another principal makes the + * server serve that request as the first principal, with nothing on the wire to show it. + *

+ * Counting distinct accepted sockets is what makes this observable: reuse and isolation are invisible in + * the responses, and the offer and poll sides deriving their key differently shows up only here. + */ +public class PooledConnectionIdentityTest { + + /** Keep-alive origin that records how many distinct connections it accepted. */ + private static final class CountingServer implements AutoCloseable { + final ServerSocket socket = new ServerSocket(0); + final Set connections = ConcurrentHashMap.newKeySet(); + + CountingServer() throws IOException { + Thread t = new Thread(this::accept); + t.setDaemon(true); + t.start(); + } + + int port() { + return socket.getLocalPort(); + } + + private void accept() { + while (!socket.isClosed()) { + final Socket s; + try { + s = socket.accept(); + } catch (IOException e) { + return; + } + connections.add(s.getPort()); + Thread w = new Thread(() -> serve(s)); + w.setDaemon(true); + w.start(); + } + } + + private void serve(Socket s) { + try (Socket conn = s) { + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), ISO_8859_1)); + String line; + while ((line = in.readLine()) != null) { + if (line.isEmpty()) { + conn.getOutputStream().write("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n".getBytes(ISO_8859_1)); + conn.getOutputStream().flush(); + } + } + } catch (IOException ignored) { + // client hung up + } + } + + @Override + public void close() throws IOException { + socket.close(); + } + } + + private static Realm ntlm(String principal) { + return new Realm.Builder(principal, "secret") + .setScheme(Realm.AuthScheme.NTLM) + .setUsePreemptiveAuth(true) + .setNtlmDomain("DOMAIN") + .setNtlmHost("host") + .build(); + } + + @Test + public void twoPrincipalsDoNotShareAPooledConnection() throws Exception { + try (CountingServer server = new CountingServer(); + AsyncHttpClient client = Dsl.asyncHttpClient(Dsl.config().setMaxRequestRetry(0))) { + String url = "http://127.0.0.1:" + server.port() + "/"; + + client.prepareGet(url).setRealm(ntlm("alice")).execute().get(30, TimeUnit.SECONDS); + client.prepareGet(url).setRealm(ntlm("bob")).execute().get(30, TimeUnit.SECONDS); + + assertEquals(server.connections.size(), 2, + "bob must not be served over the socket alice authenticated"); + } + } + + /** + * The isolation must not cost every other request its connection reuse, and one identity must keep + * reusing its own. + */ + @Test + public void oneIdentityStillReusesItsOwnConnection() throws Exception { + try (CountingServer server = new CountingServer(); + AsyncHttpClient client = Dsl.asyncHttpClient(Dsl.config().setMaxRequestRetry(0))) { + String url = "http://127.0.0.1:" + server.port() + "/"; + + client.prepareGet(url).setRealm(ntlm("alice")).execute().get(30, TimeUnit.SECONDS); + client.prepareGet(url).setRealm(ntlm("alice")).execute().get(30, TimeUnit.SECONDS); + + assertEquals(server.connections.size(), 1, + "alice's second request must reuse her own pooled connection"); + } + } + + @Test + public void requestsWithoutARealmStillReuseAConnection() throws Exception { + try (CountingServer server = new CountingServer(); + AsyncHttpClient client = Dsl.asyncHttpClient(Dsl.config().setMaxRequestRetry(0))) { + String url = "http://127.0.0.1:" + server.port() + "/"; + + client.prepareGet(url).execute().get(30, TimeUnit.SECONDS); + client.prepareGet(url).execute().get(30, TimeUnit.SECONDS); + + assertEquals(server.connections.size(), 1, "ordinary pooling must be unaffected"); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKeyTest.java b/client/src/test/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKeyTest.java new file mode 100644 index 0000000000..296c51d27f --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/channel/PrincipalScopedPartitionKeyTest.java @@ -0,0 +1,89 @@ +/* + * 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.channel; + +import org.asynchttpclient.Dsl; +import org.asynchttpclient.Realm; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertSame; + +/** + * NTLM and Negotiate authenticate the connection rather than the request, so two principals must never + * share a pooled socket. Schemes that authenticate each request must keep sharing them, or every one of + * them loses connection reuse. + */ +public class PrincipalScopedPartitionKeyTest { + + private static final Object BASE = "host:443"; + + private static Realm realm(Realm.AuthScheme scheme, String principal) { + return new Realm.Builder(principal, "secret").setScheme(scheme).build(); + } + + @Test + public void twoPrincipalsOnAConnectionScopedSchemeDoNotShareAKey() { + for (Realm.AuthScheme scheme : new Realm.AuthScheme[]{ + Realm.AuthScheme.NTLM, Realm.AuthScheme.KERBEROS, Realm.AuthScheme.SPNEGO}) { + Object alice = PrincipalScopedPartitionKey.scope(BASE, realm(scheme, "alice")); + Object bob = PrincipalScopedPartitionKey.scope(BASE, realm(scheme, "bob")); + + assertNotEquals(alice, bob, scheme + ": one principal's connection must not be reused by another"); + assertNotEquals(BASE, alice, scheme + ": the scoped key must differ from the unscoped one"); + } + } + + @Test + public void theSamePrincipalKeepsTheSameKey() { + Object first = PrincipalScopedPartitionKey.scope(BASE, realm(Realm.AuthScheme.NTLM, "alice")); + Object second = PrincipalScopedPartitionKey.scope(BASE, realm(Realm.AuthScheme.NTLM, "alice")); + + assertEquals(first, second, "the same principal must keep reusing its own connections"); + assertEquals(second.hashCode(), first.hashCode()); + } + + /** + * Basic and Digest send credentials with every request, so their connections are not tied to an + * identity. Scoping them would cost reuse for no benefit. + */ + @Test + public void requestScopedSchemesAreLeftAlone() { + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, realm(Realm.AuthScheme.BASIC, "alice"))); + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, realm(Realm.AuthScheme.DIGEST, "alice"))); + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, Dsl.basicAuthRealm("alice", "s").build())); + } + + @Test + public void noRealmAndNoPrincipalAreLeftAlone() { + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, null)); + assertSame(BASE, PrincipalScopedPartitionKey.scope(BASE, + new Realm.Builder(null, null).setScheme(Realm.AuthScheme.NTLM).build())); + } + + /** + * Different hosts must stay separate even for one principal, or the scoping would collapse the + * distinction the base key exists to make. + */ + @Test + public void theBaseKeyStillSeparatesHosts() { + Realm alice = realm(Realm.AuthScheme.NTLM, "alice"); + + assertNotEquals(PrincipalScopedPartitionKey.scope("host-a:443", alice), + PrincipalScopedPartitionKey.scope("host-b:443", alice)); + } +} From d1f0ccec417092098d40242fee7dfac84bb3c21f Mon Sep 17 00:00:00 2001 From: Aayush Atharva Date: Sun, 9 Aug 2026 22:45:22 +0000 Subject: [PATCH 32/32] Refuse a cookie whose Domain names a public suffix RFC 6265 section 5.3 implemented step 6 but not step 5, so evil.co.uk could set Domain=co.uk and every other host under that registry received the cookie. Bundle the ICANN section of the Mozilla list, with its licence header and section markers intact, and reject a Domain that matches it. Step 5 keeps a cookie whose Domain equals the request host, so single-label hosts such as dev, app and cloud, which are ICANN suffixes and also the short names Docker Compose and Kubernetes hand out, still work. Match with Locale.ROOT: under Turkish "INFO".toLowerCase() is not info, so every I-initial suffix would stop matching and the guard would fail open. If the list cannot be read nothing is treated as a public suffix. --- LICENSES/LICENSE.public-suffix-list.txt | 13 + .../cookie/PublicSuffixList.java | 124 + .../cookie/ThreadSafeCookieStore.java | 12 + .../cookie/public_suffix_list.dat | 11255 ++++++++++++++++ .../cookie/PublicSuffixCookieTest.java | 125 + 5 files changed, 11529 insertions(+) create mode 100644 LICENSES/LICENSE.public-suffix-list.txt create mode 100644 client/src/main/java/org/asynchttpclient/cookie/PublicSuffixList.java create mode 100644 client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat create mode 100644 client/src/test/java/org/asynchttpclient/cookie/PublicSuffixCookieTest.java diff --git a/LICENSES/LICENSE.public-suffix-list.txt b/LICENSES/LICENSE.public-suffix-list.txt new file mode 100644 index 0000000000..625e64ae87 --- /dev/null +++ b/LICENSES/LICENSE.public-suffix-list.txt @@ -0,0 +1,13 @@ +The file client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat is a copy of the +ICANN section of the Public Suffix List, maintained by the Mozilla Foundation and distributed from +https://publicsuffix.org/list/public_suffix_list.dat. + +It is used to implement RFC 6265 section 5.3 step 5, which requires a cookie naming a public suffix +in its Domain attribute to be ignored. + +Only the ICANN section is included; the private section is omitted. The upstream licence header and +the section markers are preserved in the file. No rules were altered. + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/client/src/main/java/org/asynchttpclient/cookie/PublicSuffixList.java b/client/src/main/java/org/asynchttpclient/cookie/PublicSuffixList.java new file mode 100644 index 0000000000..66d0ef4d6b --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/cookie/PublicSuffixList.java @@ -0,0 +1,124 @@ +/* + * 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.cookie; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Collections; +import java.util.Locale; +import java.util.HashSet; +import java.util.Set; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** + * The ICANN section of the Mozilla Public Suffix List, used to decide whether a cookie {@code Domain} + * attribute names a registry rather than a site. + * + *

RFC 6265 Section 5.3 step 5 requires rejecting a {@code Domain} that is a public suffix, and that + * rule cannot be approximated: {@code co.uk} has a dot like any ordinary domain, so counting labels does + * not distinguish a registry from a site. Without the list a host under {@code co.uk} can set a cookie for + * {@code co.uk} itself and every other host under that suffix receives it. + * + *

Only the ICANN section is bundled. The private section describes organisations that let others + * register names beneath them, which is a weaker property than a registry and not what step 5 is about. + * + *

Matching lowercases with {@link Locale#ROOT}. The default locale would be wrong here in a way that + * matters: under Turkish, {@code "INFO".toLowerCase()} is not {@code info}, so the check would answer + * false for every I-initial suffix and fail open exactly where it is meant to hold. + * + *

The list is data and goes stale as registries change. A suffix added upstream after this release is + * not recognised until the bundled copy is refreshed, so this narrows the exposure rather than closing it + * for all time. If the resource cannot be read the check reports nothing as a public suffix, leaving + * behaviour as it was rather than rejecting cookies that used to work. + */ +public final class PublicSuffixList { + + private static final Logger LOGGER = LoggerFactory.getLogger(PublicSuffixList.class); + private static final String RESOURCE = "/org/asynchttpclient/cookie/public_suffix_list.dat"; + + private static final Set EXACT; + private static final Set WILDCARD; + private static final Set EXCEPTIONS; + + static { + Set exact = new HashSet<>(8192); + Set wildcard = new HashSet<>(32); + Set exceptions = new HashSet<>(16); + try (InputStream in = PublicSuffixList.class.getResourceAsStream(RESOURCE)) { + if (in == null) { + LOGGER.warn("Public suffix list {} is missing; a cookie Domain naming a public suffix " + + "cannot be rejected", RESOURCE); + } else { + BufferedReader reader = new BufferedReader(new InputStreamReader(in, UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + String rule = line.trim(); + if (rule.isEmpty() || rule.startsWith("//")) { + continue; + } + if (rule.charAt(0) == '!') { + exceptions.add(rule.substring(1).toLowerCase(Locale.ROOT)); + } else if (rule.startsWith("*.")) { + wildcard.add(rule.substring(2).toLowerCase(Locale.ROOT)); + } else { + exact.add(rule.toLowerCase(Locale.ROOT)); + } + } + } + } catch (IOException e) { + LOGGER.warn("Could not read the public suffix list; a cookie Domain naming a public suffix " + + "cannot be rejected", e); + } + EXACT = Collections.unmodifiableSet(exact); + WILDCARD = Collections.unmodifiableSet(wildcard); + EXCEPTIONS = Collections.unmodifiableSet(exceptions); + } + + private PublicSuffixList() { + } + + /** + * Whether {@code domain} is a public suffix, and so may not be the {@code Domain} of a cookie. + * + * @param domain a hostname, without a leading dot + */ + public static boolean isPublicSuffix(String domain) { + if (domain == null || domain.isEmpty()) { + return false; + } + String candidate = domain.toLowerCase(Locale.ROOT); + if (candidate.charAt(candidate.length() - 1) == '.') { + candidate = candidate.substring(0, candidate.length() - 1); + } + // An exception rule names something that IS registrable despite matching a wildcard above it. + if (EXCEPTIONS.contains(candidate)) { + return false; + } + if (EXACT.contains(candidate)) { + return true; + } + // A wildcard rule such as *.ck makes every direct child of ck a suffix, so the candidate is one + // when its parent carries the rule. + int dot = candidate.indexOf('.'); + return dot > 0 && WILDCARD.contains(candidate.substring(dot + 1)); + } +} diff --git a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java index 9e2e6a658d..25c07f3d13 100644 --- a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java +++ b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java @@ -177,6 +177,18 @@ private void add(String requestDomain, String requestPath, Cookie cookie) { return; } + // rfc6265#section-5.3 step 5: a Domain naming a public suffix must not be honoured. Step 6 above only + // asks whether the request host sits under the Domain, which evil.co.uk setting Domain=co.uk + // satisfies, so on its own it still lets one site plant a cookie every other site under that registry + // receives. Label counting cannot stand in: co.uk has a dot like any other domain. + // + // The step also keeps a cookie whose Domain equals the request host, as a host-only cookie. Dropping + // it would break ordinary single-label hosts: dev, app, box and cloud are ICANN suffixes as well as + // the short names Docker Compose and Kubernetes hand out. + if (!hostOnly && PublicSuffixList.isPublicSuffix(keyDomain) && !keyDomain.equals(requestDomain)) { + return; + } + String keyPath = cookiePath(cookie.path(), requestPath); CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyPath); diff --git a/client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat b/client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat new file mode 100644 index 0000000000..0827ebab62 --- /dev/null +++ b/client/src/main/resources/org/asynchttpclient/cookie/public_suffix_list.dat @@ -0,0 +1,11255 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +// Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat, +// rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported. + +// Instructions on pulling and using this list can be found at https://publicsuffix.org/list/. + +// ===BEGIN ICANN DOMAINS=== + +// ac : http://nic.ac/rules.htm +ac +com.ac +edu.ac +gov.ac +mil.ac +net.ac +org.ac + +// ad : https://www.iana.org/domains/root/db/ad.html +// Confirmed by Amadeu Abril i Abril (CORE) 2024-11-17 +ad + +// ae : https://www.iana.org/domains/root/db/ae.html +ae +ac.ae +co.ae +gov.ae +mil.ae +net.ae +org.ae +sch.ae + +// aero : https://information.aero/registration/policies/dmp +aero +// 2LDs +airline.aero +airport.aero +// 2LDs (currently not accepting registration, seemingly never have) +// As of 2024-07, these are marked as reserved for potential 3LD +// registrations (clause 11 "allocated subdomains" in the 2006 TLD +// policy), but the relevant industry partners have not opened them up +// for registration. Current status can be determined from the TLD's +// policy document: 2LDs that are open for registration must list +// their policy in the TLD's policy. Any 2LD without such a policy is +// not open for registrations. +accident-investigation.aero +accident-prevention.aero +aerobatic.aero +aeroclub.aero +aerodrome.aero +agents.aero +air-surveillance.aero +air-traffic-control.aero +aircraft.aero +airtraffic.aero +ambulance.aero +association.aero +author.aero +ballooning.aero +broker.aero +caa.aero +cargo.aero +catering.aero +certification.aero +championship.aero +charter.aero +civilaviation.aero +club.aero +conference.aero +consultant.aero +consulting.aero +control.aero +council.aero +crew.aero +design.aero +dgca.aero +educator.aero +emergency.aero +engine.aero +engineer.aero +entertainment.aero +equipment.aero +exchange.aero +express.aero +federation.aero +flight.aero +freight.aero +fuel.aero +gliding.aero +government.aero +groundhandling.aero +group.aero +hanggliding.aero +homebuilt.aero +insurance.aero +journal.aero +journalist.aero +leasing.aero +logistics.aero +magazine.aero +maintenance.aero +marketplace.aero +media.aero +microlight.aero +modelling.aero +navigation.aero +parachuting.aero +paragliding.aero +passenger-association.aero +pilot.aero +press.aero +production.aero +recreation.aero +repbody.aero +res.aero +research.aero +rotorcraft.aero +safety.aero +scientist.aero +services.aero +show.aero +skydiving.aero +software.aero +student.aero +taxi.aero +trader.aero +trading.aero +trainer.aero +union.aero +workinggroup.aero +works.aero + +// af : https://www.nic.af/domain-price +af +com.af +edu.af +gov.af +net.af +org.af + +// ag : http://www.nic.ag/prices.htm +ag +co.ag +com.ag +net.ag +nom.ag +org.ag + +// ai : http://nic.com.ai/ +ai +com.ai +net.ai +off.ai +org.ai + +// al : http://www.ert.gov.al/ert_alb/faq_det.html?Id=31 +al +com.al +edu.al +gov.al +mil.al +net.al +org.al + +// am : https://www.amnic.net/policy/en/Policy_EN.pdf +// Confirmed by ISOC AM 2024-11-18 +am +co.am +com.am +commune.am +net.am +org.am + +// ao : https://www.iana.org/domains/root/db/ao.html +// https://www.dns.ao/ao/ +ao +co.ao +ed.ao +edu.ao +gov.ao +gv.ao +it.ao +og.ao +org.ao +pb.ao + +// aq : https://www.iana.org/domains/root/db/aq.html +aq + +// ar : https://nic.ar/es/nic-argentina/normativa +ar +bet.ar +com.ar +coop.ar +edu.ar +gob.ar +gov.ar +int.ar +mil.ar +musica.ar +mutual.ar +net.ar +org.ar +seg.ar +senasa.ar +tur.ar + +// arpa : https://www.iana.org/domains/root/db/arpa.html +// Confirmed by registry 2008-06-18 +arpa +e164.arpa +home.arpa +in-addr.arpa +ip6.arpa +iris.arpa +uri.arpa +urn.arpa + +// as : https://www.iana.org/domains/root/db/as.html +as +gov.as + +// asia : https://www.iana.org/domains/root/db/asia.html +asia + +// at : https://www.iana.org/domains/root/db/at.html +// Confirmed by registry 2008-06-17 +at +ac.at +sth.ac.at +co.at +gv.at +or.at + +// au : https://www.iana.org/domains/root/db/au.html +// https://www.auda.org.au/ +// Confirmed by registry 2025-07-16 +au +// 2LDs +asn.au +com.au +edu.au +gov.au +id.au +net.au +org.au +// Historic 2LDs (closed to new registration, but sites still exist) +conf.au +oz.au +// CGDNs : https://www.auda.org.au/au-domain-names/the-different-au-domain-names/state-and-territory-domain-names/ +act.au +nsw.au +nt.au +qld.au +sa.au +tas.au +vic.au +wa.au +// 3LDs +act.edu.au +catholic.edu.au +// eq.edu.au - Removed at the request of the Queensland Department of Education +nsw.edu.au +nt.edu.au +qld.edu.au +sa.edu.au +tas.edu.au +vic.edu.au +wa.edu.au +// act.gov.au - Bug 984824 - Removed at request of Greg Tankard +// nsw.gov.au - Bug 547985 - Removed at request of +// nt.gov.au - Bug 940478 - Removed at request of Greg Connors +qld.gov.au +sa.gov.au +tas.gov.au +vic.gov.au +wa.gov.au +// 4LDs +// education.tas.edu.au - Removed at the request of the Department of Education Tasmania +// schools.nsw.edu.au - Removed at the request of the New South Wales Department of Education. + +// aw : https://www.iana.org/domains/root/db/aw.html +aw +com.aw + +// ax : https://www.iana.org/domains/root/db/ax.html +ax + +// az : https://www.iana.org/domains/root/db/az.html +// Confirmed via https://whois.az/?page_id=10 2024-12-11 +az +biz.az +co.az +com.az +edu.az +gov.az +info.az +int.az +mil.az +name.az +net.az +org.az +pp.az +// No longer available for registration, however domains exist as of 2024-12-11 +// see https://whois.az/?page_id=783 +pro.az + +// ba : https://www.iana.org/domains/root/db/ba.html +ba +com.ba +edu.ba +gov.ba +mil.ba +net.ba +org.ba + +// bb : https://www.iana.org/domains/root/db/bb.html +bb +biz.bb +co.bb +com.bb +edu.bb +gov.bb +info.bb +net.bb +org.bb +store.bb +tv.bb + +// bd : https://www.iana.org/domains/root/db/bd.html +// Confirmed by registry +bd +ac.bd +ai.bd +co.bd +com.bd +edu.bd +gov.bd +id.bd +info.bd +it.bd +mil.bd +net.bd +org.bd +sch.bd +tv.bd + +// be : https://www.iana.org/domains/root/db/be.html +// Confirmed by registry 2008-06-08 +be +ac.be + +// bf : https://www.iana.org/domains/root/db/bf.html +bf +gov.bf + +// bg : https://www.iana.org/domains/root/db/bg.html +// https://www.register.bg/user/static/rules/en/index.html +bg +0.bg +1.bg +2.bg +3.bg +4.bg +5.bg +6.bg +7.bg +8.bg +9.bg +a.bg +b.bg +c.bg +d.bg +e.bg +f.bg +g.bg +h.bg +i.bg +j.bg +k.bg +l.bg +m.bg +n.bg +o.bg +p.bg +q.bg +r.bg +s.bg +t.bg +u.bg +v.bg +w.bg +x.bg +y.bg +z.bg + +// bh : https://www.iana.org/domains/root/db/bh.html +bh +com.bh +edu.bh +gov.bh +net.bh +org.bh + +// bi : https://www.iana.org/domains/root/db/bi.html +// http://whois.nic.bi/ +bi +co.bi +com.bi +edu.bi +or.bi +org.bi + +// biz : https://www.iana.org/domains/root/db/biz.html +biz + +// bj : https://nic.bj/bj-suffixes.txt +// Submitted by registry +bj +africa.bj +agro.bj +architectes.bj +assur.bj +avocats.bj +co.bj +com.bj +eco.bj +econo.bj +edu.bj +info.bj +loisirs.bj +money.bj +net.bj +org.bj +ote.bj +restaurant.bj +resto.bj +tourism.bj +univ.bj + +// bm : https://www.bermudanic.bm/domain-registration/index.php +bm +com.bm +edu.bm +gov.bm +net.bm +org.bm + +// bn : http://www.bnnic.bn/faqs +bn +com.bn +edu.bn +gov.bn +net.bn +org.bn + +// bo : https://nic.bo +// Confirmed by registry 2024-11-19 +bo +com.bo +edu.bo +gob.bo +int.bo +mil.bo +net.bo +org.bo +tv.bo +web.bo +// Social Domains +academia.bo +agro.bo +arte.bo +blog.bo +bolivia.bo +ciencia.bo +cooperativa.bo +democracia.bo +deporte.bo +ecologia.bo +economia.bo +empresa.bo +indigena.bo +industria.bo +info.bo +medicina.bo +movimiento.bo +musica.bo +natural.bo +nombre.bo +noticias.bo +patria.bo +plurinacional.bo +politica.bo +profesional.bo +pueblo.bo +revista.bo +salud.bo +tecnologia.bo +tksat.bo +transporte.bo +wiki.bo + +// br : http://registro.br/dominio/categoria.html +// Submitted by registry +br +9guacu.br +abc.br +adm.br +adv.br +agr.br +aju.br +am.br +anani.br +aparecida.br +api.br +app.br +arq.br +art.br +ato.br +b.br +barueri.br +belem.br +bet.br +bhz.br +bib.br +bio.br +blog.br +bmd.br +boavista.br +bsb.br +campinagrande.br +campinas.br +caxias.br +cim.br +cng.br +cnt.br +com.br +contagem.br +coop.br +coz.br +cri.br +cuiaba.br +curitiba.br +def.br +des.br +det.br +dev.br +ecn.br +eco.br +edu.br +emp.br +enf.br +eng.br +esp.br +etc.br +eti.br +far.br +feira.br +flog.br +floripa.br +fm.br +fnd.br +fortal.br +fot.br +foz.br +fst.br +g12.br +geo.br +ggf.br +goiania.br +gov.br +// gov.br 26 states + df https://en.wikipedia.org/wiki/States_of_Brazil +ac.gov.br +al.gov.br +am.gov.br +ap.gov.br +ba.gov.br +ce.gov.br +df.gov.br +es.gov.br +go.gov.br +ma.gov.br +mg.gov.br +ms.gov.br +mt.gov.br +pa.gov.br +pb.gov.br +pe.gov.br +pi.gov.br +pr.gov.br +rj.gov.br +rn.gov.br +ro.gov.br +rr.gov.br +rs.gov.br +sc.gov.br +se.gov.br +sp.gov.br +to.gov.br +gru.br +ia.br +imb.br +ind.br +inf.br +jab.br +jampa.br +jdf.br +joinville.br +jor.br +jus.br +leg.br +leilao.br +lel.br +log.br +londrina.br +macapa.br +maceio.br +manaus.br +maringa.br +mat.br +med.br +mil.br +morena.br +mp.br +mus.br +natal.br +net.br +niteroi.br +*.nom.br +not.br +ntr.br +odo.br +ong.br +org.br +osasco.br +palmas.br +poa.br +ppg.br +pro.br +psc.br +psi.br +pvh.br +qsl.br +radio.br +rec.br +recife.br +rep.br +ribeirao.br +rio.br +riobranco.br +riopreto.br +salvador.br +sampa.br +santamaria.br +santoandre.br +saobernardo.br +saogonca.br +seg.br +sjc.br +slg.br +slz.br +social.br +sorocaba.br +srv.br +taxi.br +tc.br +tec.br +teo.br +the.br +tmp.br +trd.br +tur.br +tv.br +udi.br +vet.br +vix.br +vlog.br +wiki.br +xyz.br +zlg.br + +// bs : http://www.nic.bs/rules.html +bs +com.bs +edu.bs +gov.bs +net.bs +org.bs + +// bt : https://www.iana.org/domains/root/db/bt.html +bt +com.bt +edu.bt +gov.bt +net.bt +org.bt + +// bv : No registrations at this time. +// Submitted by registry +bv + +// bw : https://www.iana.org/domains/root/db/bw.html +// https://nic.net.bw/bw-name-structure +bw +ac.bw +co.bw +gov.bw +net.bw +org.bw + +// by : https://www.iana.org/domains/root/db/by.html +// http://tld.by/rules_2006_en.html +// list of other 2nd level tlds ? +by +gov.by +mil.by +// Official information does not indicate that com.by is a reserved +// second-level domain, but it's being used as one (see www.google.com.by and +// www.yahoo.com.by, for example), so we list it here for safety's sake. +com.by +// http://hoster.by/ +of.by + +// bz : https://www.iana.org/domains/root/db/bz.html +// http://www.belizenic.bz/ +bz +co.bz +com.bz +edu.bz +gov.bz +net.bz +org.bz + +// ca : https://www.iana.org/domains/root/db/ca.html +ca +// ca geographical names +ab.ca +bc.ca +mb.ca +nb.ca +nf.ca +nl.ca +ns.ca +nt.ca +nu.ca +on.ca +pe.ca +qc.ca +sk.ca +yk.ca +// gc.ca: https://en.wikipedia.org/wiki/.gc.ca +// see also: http://registry.gc.ca/en/SubdomainFAQ +gc.ca + +// cat : https://www.iana.org/domains/root/db/cat.html +cat + +// cc : https://www.iana.org/domains/root/db/cc.html +cc + +// cd : https://www.iana.org/domains/root/db/cd.html +// https://www.nic.cd +cd +gov.cd + +// cf : https://www.iana.org/domains/root/db/cf.html +cf + +// cg : https://www.iana.org/domains/root/db/cg.html +cg + +// ch : https://www.iana.org/domains/root/db/ch.html +ch + +// ci : https://www.iana.org/domains/root/db/ci.html +ci +ac.ci +aéroport.ci +asso.ci +co.ci +com.ci +ed.ci +edu.ci +go.ci +gouv.ci +int.ci +net.ci +or.ci +org.ci + +// ck : https://www.iana.org/domains/root/db/ck.html +*.ck +!www.ck + +// cl : https://www.nic.cl +// Confirmed by .CL registry +cl +co.cl +gob.cl +gov.cl +mil.cl + +// cm : https://www.iana.org/domains/root/db/cm.html plus bug 981927 +cm +co.cm +com.cm +gov.cm +net.cm + +// cn : https://www.iana.org/domains/root/db/cn.html +// Submitted by registry +cn +ac.cn +com.cn +edu.cn +gov.cn +mil.cn +net.cn +org.cn +公司.cn +網絡.cn +网络.cn +// cn geographic names +ah.cn +bj.cn +cq.cn +fj.cn +gd.cn +gs.cn +gx.cn +gz.cn +ha.cn +hb.cn +he.cn +hi.cn +hk.cn +hl.cn +hn.cn +jl.cn +js.cn +jx.cn +ln.cn +mo.cn +nm.cn +nx.cn +qh.cn +sc.cn +sd.cn +sh.cn +sn.cn +sx.cn +tj.cn +tw.cn +xj.cn +xz.cn +yn.cn +zj.cn + +// co : https://www.iana.org/domains/root/db/co.html +// https://www.cointernet.com.co/como-funciona-un-dominio-restringido +// Confirmed by registry 2024-11-18 +co +com.co +edu.co +gov.co +mil.co +net.co +nom.co +org.co + +// com : https://www.iana.org/domains/root/db/com.html +com + +// coop : https://www.iana.org/domains/root/db/coop.html +coop + +// cr : https://nic.cr/capitulo-1-registro-de-un-nombre-de-dominio/ +cr +ac.cr +co.cr +ed.cr +fi.cr +go.cr +or.cr +sa.cr + +// cu : https://www.iana.org/domains/root/db/cu.html +cu +com.cu +edu.cu +gob.cu +inf.cu +nat.cu +net.cu +org.cu + +// cv : https://www.iana.org/domains/root/db/cv.html +// https://ola.cv/domain-extensions-under-cv/ +// Confirmed by registry 2024-11-26 +cv +com.cv +edu.cv +id.cv +int.cv +net.cv +nome.cv +org.cv +publ.cv + +// cw : https://www.uoc.cw/cw-registry +// Confirmed by registry 2024-11-19 +cw +com.cw +edu.cw +net.cw +org.cw + +// cx : https://www.iana.org/domains/root/db/cx.html +// list of other 2nd level tlds ? +cx +gov.cx + +// cy : http://www.nic.cy/ +// Submitted by Panayiotou Fotia +// https://nic.cy/wp-content/uploads/2024/01/Create-Request-for-domain-name-registration-1.pdf +cy +ac.cy +biz.cy +com.cy +ekloges.cy +gov.cy +ltd.cy +mil.cy +net.cy +org.cy +press.cy +pro.cy +tm.cy + +// cz : https://www.iana.org/domains/root/db/cz.html +// Confirmed by registry 2025-08-06 +cz +gov.cz + +// de : https://www.iana.org/domains/root/db/de.html +// Confirmed by registry (with technical +// reservations) 2008-07-01 +de + +// dj : https://www.iana.org/domains/root/db/dj.html +dj + +// dk : https://www.iana.org/domains/root/db/dk.html +// Confirmed by registry 2008-06-17 +dk + +// dm : https://www.iana.org/domains/root/db/dm.html +// https://nic.dm/policies/pdf/DMRulesandGuidelines2024v1.pdf +// Confirmed by registry 2024-11-19 +dm +co.dm +com.dm +edu.dm +gov.dm +net.dm +org.dm + +// do : https://www.iana.org/domains/root/db/do.html +do +art.do +com.do +edu.do +gob.do +gov.do +mil.do +net.do +org.do +sld.do +web.do + +// dz : http://www.nic.dz/images/pdf_nic/charte.pdf +dz +art.dz +asso.dz +com.dz +edu.dz +gov.dz +net.dz +org.dz +pol.dz +soc.dz +tm.dz + +// ec : https://www.nic.ec/ +// Submitted by registry +ec +abg.ec +adm.ec +agron.ec +arqt.ec +art.ec +bar.ec +chef.ec +com.ec +cont.ec +cpa.ec +cue.ec +dent.ec +dgn.ec +disco.ec +doc.ec +edu.ec +eng.ec +esm.ec +fin.ec +fot.ec +gal.ec +gob.ec +gov.ec +gye.ec +ibr.ec +info.ec +k12.ec +lat.ec +loj.ec +med.ec +mil.ec +mktg.ec +mon.ec +net.ec +ntr.ec +odont.ec +org.ec +pro.ec +prof.ec +psic.ec +psiq.ec +pub.ec +rio.ec +rrpp.ec +sal.ec +tech.ec +tul.ec +tur.ec +uio.ec +vet.ec +xxx.ec + +// edu : https://www.iana.org/domains/root/db/edu.html +edu + +// ee : https://www.internet.ee/domains/general-domains-and-procedure-for-registration-of-sub-domains-under-general-domains +ee +aip.ee +com.ee +edu.ee +fie.ee +gov.ee +lib.ee +med.ee +org.ee +pri.ee +riik.ee + +// eg : https://www.iana.org/domains/root/db/eg.html +// https://domain.eg/en/domain-rules/subdomain-names-types/ +eg +ac.eg +com.eg +edu.eg +eun.eg +gov.eg +info.eg +me.eg +mil.eg +name.eg +net.eg +org.eg +sci.eg +sport.eg +tv.eg + +// er : https://www.iana.org/domains/root/db/er.html +*.er + +// es : https://www.dominios.es/en +es +com.es +edu.es +gob.es +nom.es +org.es + +// et : https://www.iana.org/domains/root/db/et.html +et +biz.et +com.et +edu.et +gov.et +info.et +name.et +net.et +org.et + +// eu : https://www.iana.org/domains/root/db/eu.html +eu + +// fi : https://www.iana.org/domains/root/db/fi.html +fi +// aland.fi : https://www.iana.org/domains/root/db/ax.html +// This domain is being phased out in favor of .ax. As there are still many +// domains under aland.fi, we still keep it on the list until aland.fi is +// completely removed. +aland.fi + +// fj : https://www.iana.org/domains/root/db/fj.html +fj +ac.fj +biz.fj +com.fj +edu.fj +gov.fj +id.fj +info.fj +mil.fj +name.fj +net.fj +org.fj +pro.fj + +// fk : https://www.iana.org/domains/root/db/fk.html +*.fk + +// fm : https://www.iana.org/domains/root/db/fm.html +fm +com.fm +edu.fm +net.fm +org.fm + +// fo : https://www.iana.org/domains/root/db/fo.html +fo + +// fr : https://www.afnic.fr/ https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +fr +asso.fr +com.fr +gouv.fr +nom.fr +prd.fr +tm.fr +// Other SLDs now selfmanaged out of AFNIC range. Former "domaines sectoriels", still registration suffixes +avoues.fr +cci.fr +greta.fr +huissier-justice.fr + +// ga : https://www.iana.org/domains/root/db/ga.html +ga + +// gb : This registry is effectively dormant +// Submitted by registry +gb + +// gd : https://www.iana.org/domains/root/db/gd.html +gd +edu.gd +gov.gd + +// ge : https://nic.ge/en/administrator/the-ge-domain-regulations +// Confirmed by registry 2024-11-20 +ge +com.ge +edu.ge +gov.ge +net.ge +org.ge +pvt.ge +school.ge + +// gf : https://www.iana.org/domains/root/db/gf.html +gf + +// gg : https://www.channelisles.net/register-1/register-direct +// Confirmed by registry 2013-11-28 +gg +co.gg +net.gg +org.gg + +// gh : https://www.iana.org/domains/root/db/gh.html +// https://www.nic.gh/ +// Although domains directly at second level are not possible at the moment, +// they have been possible for some time and may come back. +gh +biz.gh +com.gh +edu.gh +gov.gh +mil.gh +net.gh +org.gh + +// gi : http://www.nic.gi/rules.html +gi +com.gi +edu.gi +gov.gi +ltd.gi +mod.gi +org.gi + +// gl : https://www.iana.org/domains/root/db/gl.html +// http://nic.gl +gl +co.gl +com.gl +edu.gl +net.gl +org.gl + +// gm : http://www.nic.gm/htmlpages%5Cgm-policy.htm +gm + +// gn : http://psg.com/dns/gn/gn.txt +// Submitted by registry +gn +ac.gn +com.gn +edu.gn +gov.gn +net.gn +org.gn + +// gov : https://www.iana.org/domains/root/db/gov.html +gov + +// gp : http://www.nic.gp/index.php?lang=en +gp +asso.gp +com.gp +edu.gp +mobi.gp +net.gp +org.gp + +// gq : https://www.iana.org/domains/root/db/gq.html +gq + +// gr : https://www.iana.org/domains/root/db/gr.html +// Submitted by registry +gr +com.gr +edu.gr +gov.gr +net.gr +org.gr + +// gs : https://www.iana.org/domains/root/db/gs.html +gs + +// gt : https://www.gt/sitio/registration_policy.php?lang=en +gt +com.gt +edu.gt +gob.gt +ind.gt +mil.gt +net.gt +org.gt + +// gu : http://gadao.gov.gu/register.html +// University of Guam : https://www.uog.edu +// Submitted by uognoc@triton.uog.edu +gu +com.gu +edu.gu +gov.gu +guam.gu +info.gu +net.gu +org.gu +web.gu + +// gw : https://www.iana.org/domains/root/db/gw.html +// gw : https://nic.gw/regras/ +gw + +// gy : https://www.iana.org/domains/root/db/gy.html +// http://registry.gy/ +gy +co.gy +com.gy +edu.gy +gov.gy +net.gy +org.gy + +// hk : https://www.hkirc.hk +// Submitted by registry +hk +com.hk +edu.hk +gov.hk +idv.hk +net.hk +org.hk +个人.hk +個人.hk +公司.hk +政府.hk +敎育.hk +教育.hk +箇人.hk +組織.hk +組织.hk +網絡.hk +網络.hk +组織.hk +组织.hk +网絡.hk +网络.hk + +// hm : https://www.iana.org/domains/root/db/hm.html +hm + +// hn : https://www.iana.org/domains/root/db/hn.html +hn +com.hn +edu.hn +gob.hn +mil.hn +net.hn +org.hn + +// hr : http://www.dns.hr/documents/pdf/HRTLD-regulations.pdf +hr +com.hr +from.hr +iz.hr +name.hr + +// ht : http://www.nic.ht/info/charte.cfm +ht +adult.ht +art.ht +asso.ht +com.ht +coop.ht +edu.ht +firm.ht +gouv.ht +info.ht +med.ht +net.ht +org.ht +perso.ht +pol.ht +pro.ht +rel.ht +shop.ht + +// hu : https://www.iana.org/domains/root/db/hu.html +// Confirmed by registry 2008-06-12 +hu +2000.hu +agrar.hu +bolt.hu +casino.hu +city.hu +co.hu +erotica.hu +erotika.hu +film.hu +forum.hu +games.hu +hotel.hu +info.hu +ingatlan.hu +jogasz.hu +konyvelo.hu +lakas.hu +media.hu +news.hu +org.hu +priv.hu +reklam.hu +sex.hu +shop.hu +sport.hu +suli.hu +szex.hu +tm.hu +tozsde.hu +utazas.hu +video.hu + +// id : https://www.iana.org/domains/root/db/id.html +id +ac.id +biz.id +co.id +desa.id +go.id +kop.id +mil.id +my.id +net.id +or.id +ponpes.id +sch.id +web.id +// xn--9tfky.id (.id, Und-Bali) +ᬩᬮᬶ.id + +// ie : https://www.iana.org/domains/root/db/ie.html +ie +gov.ie + +// il : http://www.isoc.org.il/domains/ +// see also: https://en.isoc.org.il/il-cctld/registration-rules +// ISOC-IL (operated by .il Registry) +il +ac.il +co.il +gov.il +idf.il +k12.il +muni.il +net.il +org.il +// xn--4dbrk0ce ("Israel", Hebrew) : IL +ישראל +// xn--4dbgdty6c.xn--4dbrk0ce. +אקדמיה.ישראל +// xn--5dbhl8d.xn--4dbrk0ce. +ישוב.ישראל +// xn--8dbq2a.xn--4dbrk0ce. +צהל.ישראל +// xn--hebda8b.xn--4dbrk0ce. +ממשל.ישראל + +// im : https://www.nic.im/ +// Submitted by registry +im +ac.im +co.im +ltd.co.im +plc.co.im +com.im +net.im +org.im +tt.im +tv.im + +// in : https://www.iana.org/domains/root/db/in.html +// see also: https://registry.in/policies +// Please note, that nic.in is not an official eTLD, but used by most +// government institutions. +// Confirmed by Gaurav Kansal 2025-11-06 +in +5g.in +6g.in +ac.in +ai.in +am.in +bank.in +bihar.in +biz.in +business.in +ca.in +cn.in +co.in +com.in +coop.in +cs.in +delhi.in +dr.in +edu.in +er.in +fin.in +firm.in +gen.in +gov.in +gujarat.in +ind.in +info.in +int.in +internet.in +io.in +me.in +mil.in +net.in +nic.in +org.in +pg.in +post.in +pro.in +res.in +travel.in +tv.in +uk.in +up.in +us.in + +// info : https://www.iana.org/domains/root/db/info.html +info + +// int : https://www.iana.org/domains/root/db/int.html +// Confirmed by registry 2008-06-18 +int +eu.int + +// io : http://www.nic.io/rules.htm +io +co.io +com.io +edu.io +gov.io +mil.io +net.io +nom.io +org.io + +// iq : http://www.cmc.iq/english/iq/iqregister1.htm +iq +com.iq +edu.iq +gov.iq +mil.iq +net.iq +org.iq + +// ir : http://www.nic.ir/Terms_and_Conditions_ir,_Appendix_1_Domain_Rules +// Also see http://www.nic.ir/Internationalized_Domain_Names +// Two .ir entries added at request of , 2010-04-16 +ir +ac.ir +co.ir +gov.ir +id.ir +net.ir +org.ir +sch.ir +// xn--mgba3a4f16a.ir (.ir, Persian YEH) +ایران.ir +// xn--mgba3a4fra.ir (.ir, Arabic YEH) +ايران.ir + +// is : http://www.isnic.is/domain/rules.php +// Confirmed by registry 2024-11-17 +is + +// it : https://www.iana.org/domains/root/db/it.html +// https://www.nic.it/ +it +edu.it +gov.it +// Regions (3.3.1) +// https://www.nic.it/en/manage-your-it/forms-and-docs -> "Assignment and Management of domain names" +abr.it +abruzzo.it +aosta-valley.it +aostavalley.it +bas.it +basilicata.it +cal.it +calabria.it +cam.it +campania.it +emilia-romagna.it +emiliaromagna.it +emr.it +friuli-v-giulia.it +friuli-ve-giulia.it +friuli-vegiulia.it +friuli-venezia-giulia.it +friuli-veneziagiulia.it +friuli-vgiulia.it +friuliv-giulia.it +friulive-giulia.it +friulivegiulia.it +friulivenezia-giulia.it +friuliveneziagiulia.it +friulivgiulia.it +fvg.it +laz.it +lazio.it +lig.it +liguria.it +lom.it +lombardia.it +lombardy.it +lucania.it +mar.it +marche.it +mol.it +molise.it +piedmont.it +piemonte.it +pmn.it +pug.it +puglia.it +sar.it +sardegna.it +sardinia.it +sic.it +sicilia.it +sicily.it +taa.it +tos.it +toscana.it +trentin-sud-tirol.it +trentin-süd-tirol.it +trentin-sudtirol.it +trentin-südtirol.it +trentin-sued-tirol.it +trentin-suedtirol.it +trentino.it +trentino-a-adige.it +trentino-aadige.it +trentino-alto-adige.it +trentino-altoadige.it +trentino-s-tirol.it +trentino-stirol.it +trentino-sud-tirol.it +trentino-süd-tirol.it +trentino-sudtirol.it +trentino-südtirol.it +trentino-sued-tirol.it +trentino-suedtirol.it +trentinoa-adige.it +trentinoaadige.it +trentinoalto-adige.it +trentinoaltoadige.it +trentinos-tirol.it +trentinostirol.it +trentinosud-tirol.it +trentinosüd-tirol.it +trentinosudtirol.it +trentinosüdtirol.it +trentinosued-tirol.it +trentinosuedtirol.it +trentinsud-tirol.it +trentinsüd-tirol.it +trentinsudtirol.it +trentinsüdtirol.it +trentinsued-tirol.it +trentinsuedtirol.it +tuscany.it +umb.it +umbria.it +val-d-aosta.it +val-daosta.it +vald-aosta.it +valdaosta.it +valle-aosta.it +valle-d-aosta.it +valle-daosta.it +valleaosta.it +valled-aosta.it +valledaosta.it +vallee-aoste.it +vallée-aoste.it +vallee-d-aoste.it +vallée-d-aoste.it +valleeaoste.it +valléeaoste.it +valleedaoste.it +valléedaoste.it +vao.it +vda.it +ven.it +veneto.it +// Provinces (3.3.2) +ag.it +agrigento.it +al.it +alessandria.it +alto-adige.it +altoadige.it +an.it +ancona.it +andria-barletta-trani.it +andria-trani-barletta.it +andriabarlettatrani.it +andriatranibarletta.it +ao.it +aosta.it +aoste.it +ap.it +aq.it +aquila.it +ar.it +arezzo.it +ascoli-piceno.it +ascolipiceno.it +asti.it +at.it +av.it +avellino.it +ba.it +balsan.it +balsan-sudtirol.it +balsan-südtirol.it +balsan-suedtirol.it +bari.it +barletta-trani-andria.it +barlettatraniandria.it +belluno.it +benevento.it +bergamo.it +bg.it +bi.it +biella.it +bl.it +bn.it +bo.it +bologna.it +bolzano.it +bolzano-altoadige.it +bozen.it +bozen-sudtirol.it +bozen-südtirol.it +bozen-suedtirol.it +br.it +brescia.it +brindisi.it +bs.it +bt.it +bulsan.it +bulsan-sudtirol.it +bulsan-südtirol.it +bulsan-suedtirol.it +bz.it +ca.it +cagliari.it +caltanissetta.it +campidano-medio.it +campidanomedio.it +campobasso.it +carbonia-iglesias.it +carboniaiglesias.it +carrara-massa.it +carraramassa.it +caserta.it +catania.it +catanzaro.it +cb.it +ce.it +cesena-forli.it +cesena-forlì.it +cesenaforli.it +cesenaforlì.it +ch.it +chieti.it +ci.it +cl.it +cn.it +co.it +como.it +cosenza.it +cr.it +cremona.it +crotone.it +cs.it +ct.it +cuneo.it +cz.it +dell-ogliastra.it +dellogliastra.it +en.it +enna.it +fc.it +fe.it +fermo.it +ferrara.it +fg.it +fi.it +firenze.it +florence.it +fm.it +foggia.it +forli-cesena.it +forlì-cesena.it +forlicesena.it +forlìcesena.it +fr.it +frosinone.it +ge.it +genoa.it +genova.it +go.it +gorizia.it +gr.it +grosseto.it +iglesias-carbonia.it +iglesiascarbonia.it +im.it +imperia.it +is.it +isernia.it +kr.it +la-spezia.it +laquila.it +laspezia.it +latina.it +lc.it +le.it +lecce.it +lecco.it +li.it +livorno.it +lo.it +lodi.it +lt.it +lu.it +lucca.it +macerata.it +mantova.it +massa-carrara.it +massacarrara.it +matera.it +mb.it +mc.it +me.it +medio-campidano.it +mediocampidano.it +messina.it +mi.it +milan.it +milano.it +mn.it +mo.it +modena.it +monza.it +monza-brianza.it +monza-e-della-brianza.it +monzabrianza.it +monzaebrianza.it +monzaedellabrianza.it +ms.it +mt.it +na.it +naples.it +napoli.it +no.it +novara.it +nu.it +nuoro.it +og.it +ogliastra.it +olbia-tempio.it +olbiatempio.it +or.it +oristano.it +ot.it +pa.it +padova.it +padua.it +palermo.it +parma.it +pavia.it +pc.it +pd.it +pe.it +perugia.it +pesaro-urbino.it +pesarourbino.it +pescara.it +pg.it +pi.it +piacenza.it +pisa.it +pistoia.it +pn.it +po.it +pordenone.it +potenza.it +pr.it +prato.it +pt.it +pu.it +pv.it +pz.it +ra.it +ragusa.it +ravenna.it +rc.it +re.it +reggio-calabria.it +reggio-emilia.it +reggiocalabria.it +reggioemilia.it +rg.it +ri.it +rieti.it +rimini.it +rm.it +rn.it +ro.it +roma.it +rome.it +rovigo.it +sa.it +salerno.it +sassari.it +savona.it +si.it +siena.it +siracusa.it +so.it +sondrio.it +sp.it +sr.it +ss.it +südtirol.it +suedtirol.it +sv.it +ta.it +taranto.it +te.it +tempio-olbia.it +tempioolbia.it +teramo.it +terni.it +tn.it +to.it +torino.it +tp.it +tr.it +trani-andria-barletta.it +trani-barletta-andria.it +traniandriabarletta.it +tranibarlettaandria.it +trapani.it +trento.it +treviso.it +trieste.it +ts.it +turin.it +tv.it +ud.it +udine.it +urbino-pesaro.it +urbinopesaro.it +va.it +varese.it +vb.it +vc.it +ve.it +venezia.it +venice.it +verbania.it +vercelli.it +verona.it +vi.it +vibo-valentia.it +vibovalentia.it +vicenza.it +viterbo.it +vr.it +vs.it +vt.it +vv.it + +// je : https://www.iana.org/domains/root/db/je.html +// Confirmed by registry 2013-11-28 +je +co.je +net.je +org.je + +// jm : http://www.com.jm/register.html +*.jm + +// jo : https://www.dns.jo/JoFamily.aspx +// Confirmed by registry 2024-11-17 +jo +agri.jo +ai.jo +com.jo +edu.jo +eng.jo +fm.jo +gov.jo +mil.jo +net.jo +org.jo +per.jo +phd.jo +sch.jo +tv.jo + +// jobs : https://www.iana.org/domains/root/db/jobs.html +jobs + +// jp : https://www.iana.org/domains/root/db/jp.html +// http://jprs.co.jp/en/jpdomain.html +// Confirmed by registry 2024-11-22 +jp +// jp organizational type names +ac.jp +ad.jp +co.jp +ed.jp +go.jp +gr.jp +lg.jp +ne.jp +or.jp +// jp prefecture type names +aichi.jp +akita.jp +aomori.jp +chiba.jp +ehime.jp +fukui.jp +fukuoka.jp +fukushima.jp +gifu.jp +gunma.jp +hiroshima.jp +hokkaido.jp +hyogo.jp +ibaraki.jp +ishikawa.jp +iwate.jp +kagawa.jp +kagoshima.jp +kanagawa.jp +kochi.jp +kumamoto.jp +kyoto.jp +mie.jp +miyagi.jp +miyazaki.jp +nagano.jp +nagasaki.jp +nara.jp +niigata.jp +oita.jp +okayama.jp +okinawa.jp +osaka.jp +saga.jp +saitama.jp +shiga.jp +shimane.jp +shizuoka.jp +tochigi.jp +tokushima.jp +tokyo.jp +tottori.jp +toyama.jp +wakayama.jp +yamagata.jp +yamaguchi.jp +yamanashi.jp +三重.jp +京都.jp +佐賀.jp +兵庫.jp +北海道.jp +千葉.jp +和歌山.jp +埼玉.jp +大分.jp +大阪.jp +奈良.jp +宮城.jp +宮崎.jp +富山.jp +山口.jp +山形.jp +山梨.jp +岐阜.jp +岡山.jp +岩手.jp +島根.jp +広島.jp +徳島.jp +愛媛.jp +愛知.jp +新潟.jp +東京.jp +栃木.jp +沖縄.jp +滋賀.jp +熊本.jp +石川.jp +神奈川.jp +福井.jp +福岡.jp +福島.jp +秋田.jp +群馬.jp +茨城.jp +長崎.jp +長野.jp +青森.jp +静岡.jp +香川.jp +高知.jp +鳥取.jp +鹿児島.jp +// jp geographic type names +// http://jprs.jp/doc/rule/saisoku-1.html +// 2024-11-22: JPRS confirmed that jp geographic type names no longer accept new registrations. +// Once all existing registrations expire (marking full discontinuation), these suffixes +// will be removed from the PSL. +*.kawasaki.jp +!city.kawasaki.jp +*.kitakyushu.jp +!city.kitakyushu.jp +*.kobe.jp +!city.kobe.jp +*.nagoya.jp +!city.nagoya.jp +*.sapporo.jp +!city.sapporo.jp +*.sendai.jp +!city.sendai.jp +*.yokohama.jp +!city.yokohama.jp +// 4th level registration +aisai.aichi.jp +ama.aichi.jp +anjo.aichi.jp +asuke.aichi.jp +chiryu.aichi.jp +chita.aichi.jp +fuso.aichi.jp +gamagori.aichi.jp +handa.aichi.jp +hazu.aichi.jp +hekinan.aichi.jp +higashiura.aichi.jp +ichinomiya.aichi.jp +inazawa.aichi.jp +inuyama.aichi.jp +isshiki.aichi.jp +iwakura.aichi.jp +kanie.aichi.jp +kariya.aichi.jp +kasugai.aichi.jp +kira.aichi.jp +kiyosu.aichi.jp +komaki.aichi.jp +konan.aichi.jp +kota.aichi.jp +mihama.aichi.jp +miyoshi.aichi.jp +nishio.aichi.jp +nisshin.aichi.jp +obu.aichi.jp +oguchi.aichi.jp +oharu.aichi.jp +okazaki.aichi.jp +owariasahi.aichi.jp +seto.aichi.jp +shikatsu.aichi.jp +shinshiro.aichi.jp +shitara.aichi.jp +tahara.aichi.jp +takahama.aichi.jp +tobishima.aichi.jp +toei.aichi.jp +togo.aichi.jp +tokai.aichi.jp +tokoname.aichi.jp +toyoake.aichi.jp +toyohashi.aichi.jp +toyokawa.aichi.jp +toyone.aichi.jp +toyota.aichi.jp +tsushima.aichi.jp +yatomi.aichi.jp +akita.akita.jp +daisen.akita.jp +fujisato.akita.jp +gojome.akita.jp +hachirogata.akita.jp +happou.akita.jp +higashinaruse.akita.jp +honjo.akita.jp +honjyo.akita.jp +ikawa.akita.jp +kamikoani.akita.jp +kamioka.akita.jp +katagami.akita.jp +kazuno.akita.jp +kitaakita.akita.jp +kosaka.akita.jp +kyowa.akita.jp +misato.akita.jp +mitane.akita.jp +moriyoshi.akita.jp +nikaho.akita.jp +noshiro.akita.jp +odate.akita.jp +oga.akita.jp +ogata.akita.jp +semboku.akita.jp +yokote.akita.jp +yurihonjo.akita.jp +aomori.aomori.jp +gonohe.aomori.jp +hachinohe.aomori.jp +hashikami.aomori.jp +hiranai.aomori.jp +hirosaki.aomori.jp +itayanagi.aomori.jp +kuroishi.aomori.jp +misawa.aomori.jp +mutsu.aomori.jp +nakadomari.aomori.jp +noheji.aomori.jp +oirase.aomori.jp +owani.aomori.jp +rokunohe.aomori.jp +sannohe.aomori.jp +shichinohe.aomori.jp +shingo.aomori.jp +takko.aomori.jp +towada.aomori.jp +tsugaru.aomori.jp +tsuruta.aomori.jp +abiko.chiba.jp +asahi.chiba.jp +chonan.chiba.jp +chosei.chiba.jp +choshi.chiba.jp +chuo.chiba.jp +funabashi.chiba.jp +futtsu.chiba.jp +hanamigawa.chiba.jp +ichihara.chiba.jp +ichikawa.chiba.jp +ichinomiya.chiba.jp +inzai.chiba.jp +isumi.chiba.jp +kamagaya.chiba.jp +kamogawa.chiba.jp +kashiwa.chiba.jp +katori.chiba.jp +katsuura.chiba.jp +kimitsu.chiba.jp +kisarazu.chiba.jp +kozaki.chiba.jp +kujukuri.chiba.jp +kyonan.chiba.jp +matsudo.chiba.jp +midori.chiba.jp +mihama.chiba.jp +minamiboso.chiba.jp +mobara.chiba.jp +mutsuzawa.chiba.jp +nagara.chiba.jp +nagareyama.chiba.jp +narashino.chiba.jp +narita.chiba.jp +noda.chiba.jp +oamishirasato.chiba.jp +omigawa.chiba.jp +onjuku.chiba.jp +otaki.chiba.jp +sakae.chiba.jp +sakura.chiba.jp +shimofusa.chiba.jp +shirako.chiba.jp +shiroi.chiba.jp +shisui.chiba.jp +sodegaura.chiba.jp +sosa.chiba.jp +tako.chiba.jp +tateyama.chiba.jp +togane.chiba.jp +tohnosho.chiba.jp +tomisato.chiba.jp +urayasu.chiba.jp +yachimata.chiba.jp +yachiyo.chiba.jp +yokaichiba.chiba.jp +yokoshibahikari.chiba.jp +yotsukaido.chiba.jp +ainan.ehime.jp +honai.ehime.jp +ikata.ehime.jp +imabari.ehime.jp +iyo.ehime.jp +kamijima.ehime.jp +kihoku.ehime.jp +kumakogen.ehime.jp +masaki.ehime.jp +matsuno.ehime.jp +matsuyama.ehime.jp +namikata.ehime.jp +niihama.ehime.jp +ozu.ehime.jp +saijo.ehime.jp +seiyo.ehime.jp +shikokuchuo.ehime.jp +tobe.ehime.jp +toon.ehime.jp +uchiko.ehime.jp +uwajima.ehime.jp +yawatahama.ehime.jp +echizen.fukui.jp +eiheiji.fukui.jp +fukui.fukui.jp +ikeda.fukui.jp +katsuyama.fukui.jp +mihama.fukui.jp +minamiechizen.fukui.jp +obama.fukui.jp +ohi.fukui.jp +ono.fukui.jp +sabae.fukui.jp +sakai.fukui.jp +takahama.fukui.jp +tsuruga.fukui.jp +wakasa.fukui.jp +ashiya.fukuoka.jp +buzen.fukuoka.jp +chikugo.fukuoka.jp +chikuho.fukuoka.jp +chikujo.fukuoka.jp +chikushino.fukuoka.jp +chikuzen.fukuoka.jp +chuo.fukuoka.jp +dazaifu.fukuoka.jp +fukuchi.fukuoka.jp +hakata.fukuoka.jp +higashi.fukuoka.jp +hirokawa.fukuoka.jp +hisayama.fukuoka.jp +iizuka.fukuoka.jp +inatsuki.fukuoka.jp +kaho.fukuoka.jp +kasuga.fukuoka.jp +kasuya.fukuoka.jp +kawara.fukuoka.jp +keisen.fukuoka.jp +koga.fukuoka.jp +kurate.fukuoka.jp +kurogi.fukuoka.jp +kurume.fukuoka.jp +minami.fukuoka.jp +miyako.fukuoka.jp +miyama.fukuoka.jp +miyawaka.fukuoka.jp +mizumaki.fukuoka.jp +munakata.fukuoka.jp +nakagawa.fukuoka.jp +nakama.fukuoka.jp +nishi.fukuoka.jp +nogata.fukuoka.jp +ogori.fukuoka.jp +okagaki.fukuoka.jp +okawa.fukuoka.jp +oki.fukuoka.jp +omuta.fukuoka.jp +onga.fukuoka.jp +onojo.fukuoka.jp +oto.fukuoka.jp +saigawa.fukuoka.jp +sasaguri.fukuoka.jp +shingu.fukuoka.jp +shinyoshitomi.fukuoka.jp +shonai.fukuoka.jp +soeda.fukuoka.jp +sue.fukuoka.jp +tachiarai.fukuoka.jp +tagawa.fukuoka.jp +takata.fukuoka.jp +toho.fukuoka.jp +toyotsu.fukuoka.jp +tsuiki.fukuoka.jp +ukiha.fukuoka.jp +umi.fukuoka.jp +usui.fukuoka.jp +yamada.fukuoka.jp +yame.fukuoka.jp +yanagawa.fukuoka.jp +yukuhashi.fukuoka.jp +aizubange.fukushima.jp +aizumisato.fukushima.jp +aizuwakamatsu.fukushima.jp +asakawa.fukushima.jp +bandai.fukushima.jp +date.fukushima.jp +fukushima.fukushima.jp +furudono.fukushima.jp +futaba.fukushima.jp +hanawa.fukushima.jp +higashi.fukushima.jp +hirata.fukushima.jp +hirono.fukushima.jp +iitate.fukushima.jp +inawashiro.fukushima.jp +ishikawa.fukushima.jp +iwaki.fukushima.jp +izumizaki.fukushima.jp +kagamiishi.fukushima.jp +kaneyama.fukushima.jp +kawamata.fukushima.jp +kitakata.fukushima.jp +kitashiobara.fukushima.jp +koori.fukushima.jp +koriyama.fukushima.jp +kunimi.fukushima.jp +miharu.fukushima.jp +mishima.fukushima.jp +namie.fukushima.jp +nango.fukushima.jp +nishiaizu.fukushima.jp +nishigo.fukushima.jp +okuma.fukushima.jp +omotego.fukushima.jp +ono.fukushima.jp +otama.fukushima.jp +samegawa.fukushima.jp +shimogo.fukushima.jp +shirakawa.fukushima.jp +showa.fukushima.jp +soma.fukushima.jp +sukagawa.fukushima.jp +taishin.fukushima.jp +tamakawa.fukushima.jp +tanagura.fukushima.jp +tenei.fukushima.jp +yabuki.fukushima.jp +yamato.fukushima.jp +yamatsuri.fukushima.jp +yanaizu.fukushima.jp +yugawa.fukushima.jp +anpachi.gifu.jp +ena.gifu.jp +gifu.gifu.jp +ginan.gifu.jp +godo.gifu.jp +gujo.gifu.jp +hashima.gifu.jp +hichiso.gifu.jp +hida.gifu.jp +higashishirakawa.gifu.jp +ibigawa.gifu.jp +ikeda.gifu.jp +kakamigahara.gifu.jp +kani.gifu.jp +kasahara.gifu.jp +kasamatsu.gifu.jp +kawaue.gifu.jp +kitagata.gifu.jp +mino.gifu.jp +minokamo.gifu.jp +mitake.gifu.jp +mizunami.gifu.jp +motosu.gifu.jp +nakatsugawa.gifu.jp +ogaki.gifu.jp +sakahogi.gifu.jp +seki.gifu.jp +sekigahara.gifu.jp +shirakawa.gifu.jp +tajimi.gifu.jp +takayama.gifu.jp +tarui.gifu.jp +toki.gifu.jp +tomika.gifu.jp +wanouchi.gifu.jp +yamagata.gifu.jp +yaotsu.gifu.jp +yoro.gifu.jp +annaka.gunma.jp +chiyoda.gunma.jp +fujioka.gunma.jp +higashiagatsuma.gunma.jp +isesaki.gunma.jp +itakura.gunma.jp +kanna.gunma.jp +kanra.gunma.jp +katashina.gunma.jp +kawaba.gunma.jp +kiryu.gunma.jp +kusatsu.gunma.jp +maebashi.gunma.jp +meiwa.gunma.jp +midori.gunma.jp +minakami.gunma.jp +naganohara.gunma.jp +nakanojo.gunma.jp +nanmoku.gunma.jp +numata.gunma.jp +oizumi.gunma.jp +ora.gunma.jp +ota.gunma.jp +shibukawa.gunma.jp +shimonita.gunma.jp +shinto.gunma.jp +showa.gunma.jp +takasaki.gunma.jp +takayama.gunma.jp +tamamura.gunma.jp +tatebayashi.gunma.jp +tomioka.gunma.jp +tsukiyono.gunma.jp +tsumagoi.gunma.jp +ueno.gunma.jp +yoshioka.gunma.jp +asaminami.hiroshima.jp +daiwa.hiroshima.jp +etajima.hiroshima.jp +fuchu.hiroshima.jp +fukuyama.hiroshima.jp +hatsukaichi.hiroshima.jp +higashihiroshima.hiroshima.jp +hongo.hiroshima.jp +jinsekikogen.hiroshima.jp +kaita.hiroshima.jp +kui.hiroshima.jp +kumano.hiroshima.jp +kure.hiroshima.jp +mihara.hiroshima.jp +miyoshi.hiroshima.jp +naka.hiroshima.jp +onomichi.hiroshima.jp +osakikamijima.hiroshima.jp +otake.hiroshima.jp +saka.hiroshima.jp +sera.hiroshima.jp +seranishi.hiroshima.jp +shinichi.hiroshima.jp +shobara.hiroshima.jp +takehara.hiroshima.jp +abashiri.hokkaido.jp +abira.hokkaido.jp +aibetsu.hokkaido.jp +akabira.hokkaido.jp +akkeshi.hokkaido.jp +asahikawa.hokkaido.jp +ashibetsu.hokkaido.jp +ashoro.hokkaido.jp +assabu.hokkaido.jp +atsuma.hokkaido.jp +bibai.hokkaido.jp +biei.hokkaido.jp +bifuka.hokkaido.jp +bihoro.hokkaido.jp +biratori.hokkaido.jp +chippubetsu.hokkaido.jp +chitose.hokkaido.jp +date.hokkaido.jp +ebetsu.hokkaido.jp +embetsu.hokkaido.jp +eniwa.hokkaido.jp +erimo.hokkaido.jp +esan.hokkaido.jp +esashi.hokkaido.jp +fukagawa.hokkaido.jp +fukushima.hokkaido.jp +furano.hokkaido.jp +furubira.hokkaido.jp +haboro.hokkaido.jp +hakodate.hokkaido.jp +hamatonbetsu.hokkaido.jp +hidaka.hokkaido.jp +higashikagura.hokkaido.jp +higashikawa.hokkaido.jp +hiroo.hokkaido.jp +hokuryu.hokkaido.jp +hokuto.hokkaido.jp +honbetsu.hokkaido.jp +horokanai.hokkaido.jp +horonobe.hokkaido.jp +ikeda.hokkaido.jp +imakane.hokkaido.jp +ishikari.hokkaido.jp +iwamizawa.hokkaido.jp +iwanai.hokkaido.jp +kamifurano.hokkaido.jp +kamikawa.hokkaido.jp +kamishihoro.hokkaido.jp +kamisunagawa.hokkaido.jp +kamoenai.hokkaido.jp +kayabe.hokkaido.jp +kembuchi.hokkaido.jp +kikonai.hokkaido.jp +kimobetsu.hokkaido.jp +kitahiroshima.hokkaido.jp +kitami.hokkaido.jp +kiyosato.hokkaido.jp +koshimizu.hokkaido.jp +kunneppu.hokkaido.jp +kuriyama.hokkaido.jp +kuromatsunai.hokkaido.jp +kushiro.hokkaido.jp +kutchan.hokkaido.jp +kyowa.hokkaido.jp +mashike.hokkaido.jp +matsumae.hokkaido.jp +mikasa.hokkaido.jp +minamifurano.hokkaido.jp +mombetsu.hokkaido.jp +moseushi.hokkaido.jp +mukawa.hokkaido.jp +muroran.hokkaido.jp +naie.hokkaido.jp +nakagawa.hokkaido.jp +nakasatsunai.hokkaido.jp +nakatombetsu.hokkaido.jp +nanae.hokkaido.jp +nanporo.hokkaido.jp +nayoro.hokkaido.jp +nemuro.hokkaido.jp +niikappu.hokkaido.jp +niki.hokkaido.jp +nishiokoppe.hokkaido.jp +noboribetsu.hokkaido.jp +numata.hokkaido.jp +obihiro.hokkaido.jp +obira.hokkaido.jp +oketo.hokkaido.jp +okoppe.hokkaido.jp +otaru.hokkaido.jp +otobe.hokkaido.jp +otofuke.hokkaido.jp +otoineppu.hokkaido.jp +oumu.hokkaido.jp +ozora.hokkaido.jp +pippu.hokkaido.jp +rankoshi.hokkaido.jp +rebun.hokkaido.jp +rikubetsu.hokkaido.jp +rishiri.hokkaido.jp +rishirifuji.hokkaido.jp +saroma.hokkaido.jp +sarufutsu.hokkaido.jp +shakotan.hokkaido.jp +shari.hokkaido.jp +shibecha.hokkaido.jp +shibetsu.hokkaido.jp +shikabe.hokkaido.jp +shikaoi.hokkaido.jp +shimamaki.hokkaido.jp +shimizu.hokkaido.jp +shimokawa.hokkaido.jp +shinshinotsu.hokkaido.jp +shintoku.hokkaido.jp +shiranuka.hokkaido.jp +shiraoi.hokkaido.jp +shiriuchi.hokkaido.jp +sobetsu.hokkaido.jp +sunagawa.hokkaido.jp +taiki.hokkaido.jp +takasu.hokkaido.jp +takikawa.hokkaido.jp +takinoue.hokkaido.jp +teshikaga.hokkaido.jp +tobetsu.hokkaido.jp +tohma.hokkaido.jp +tomakomai.hokkaido.jp +tomari.hokkaido.jp +toya.hokkaido.jp +toyako.hokkaido.jp +toyotomi.hokkaido.jp +toyoura.hokkaido.jp +tsubetsu.hokkaido.jp +tsukigata.hokkaido.jp +urakawa.hokkaido.jp +urausu.hokkaido.jp +uryu.hokkaido.jp +utashinai.hokkaido.jp +wakkanai.hokkaido.jp +wassamu.hokkaido.jp +yakumo.hokkaido.jp +yoichi.hokkaido.jp +aioi.hyogo.jp +akashi.hyogo.jp +ako.hyogo.jp +amagasaki.hyogo.jp +aogaki.hyogo.jp +asago.hyogo.jp +ashiya.hyogo.jp +awaji.hyogo.jp +fukusaki.hyogo.jp +goshiki.hyogo.jp +harima.hyogo.jp +himeji.hyogo.jp +ichikawa.hyogo.jp +inagawa.hyogo.jp +itami.hyogo.jp +kakogawa.hyogo.jp +kamigori.hyogo.jp +kamikawa.hyogo.jp +kasai.hyogo.jp +kasuga.hyogo.jp +kawanishi.hyogo.jp +miki.hyogo.jp +minamiawaji.hyogo.jp +nishinomiya.hyogo.jp +nishiwaki.hyogo.jp +ono.hyogo.jp +sanda.hyogo.jp +sannan.hyogo.jp +sasayama.hyogo.jp +sayo.hyogo.jp +shingu.hyogo.jp +shinonsen.hyogo.jp +shiso.hyogo.jp +sumoto.hyogo.jp +taishi.hyogo.jp +taka.hyogo.jp +takarazuka.hyogo.jp +takasago.hyogo.jp +takino.hyogo.jp +tamba.hyogo.jp +tatsuno.hyogo.jp +toyooka.hyogo.jp +yabu.hyogo.jp +yashiro.hyogo.jp +yoka.hyogo.jp +yokawa.hyogo.jp +ami.ibaraki.jp +asahi.ibaraki.jp +bando.ibaraki.jp +chikusei.ibaraki.jp +daigo.ibaraki.jp +fujishiro.ibaraki.jp +hitachi.ibaraki.jp +hitachinaka.ibaraki.jp +hitachiomiya.ibaraki.jp +hitachiota.ibaraki.jp +ibaraki.ibaraki.jp +ina.ibaraki.jp +inashiki.ibaraki.jp +itako.ibaraki.jp +iwama.ibaraki.jp +joso.ibaraki.jp +kamisu.ibaraki.jp +kasama.ibaraki.jp +kashima.ibaraki.jp +kasumigaura.ibaraki.jp +koga.ibaraki.jp +miho.ibaraki.jp +mito.ibaraki.jp +moriya.ibaraki.jp +naka.ibaraki.jp +namegata.ibaraki.jp +oarai.ibaraki.jp +ogawa.ibaraki.jp +omitama.ibaraki.jp +ryugasaki.ibaraki.jp +sakai.ibaraki.jp +sakuragawa.ibaraki.jp +shimodate.ibaraki.jp +shimotsuma.ibaraki.jp +shirosato.ibaraki.jp +sowa.ibaraki.jp +suifu.ibaraki.jp +takahagi.ibaraki.jp +tamatsukuri.ibaraki.jp +tokai.ibaraki.jp +tomobe.ibaraki.jp +tone.ibaraki.jp +toride.ibaraki.jp +tsuchiura.ibaraki.jp +tsukuba.ibaraki.jp +uchihara.ibaraki.jp +ushiku.ibaraki.jp +yachiyo.ibaraki.jp +yamagata.ibaraki.jp +yawara.ibaraki.jp +yuki.ibaraki.jp +anamizu.ishikawa.jp +hakui.ishikawa.jp +hakusan.ishikawa.jp +kaga.ishikawa.jp +kahoku.ishikawa.jp +kanazawa.ishikawa.jp +kawakita.ishikawa.jp +komatsu.ishikawa.jp +nakanoto.ishikawa.jp +nanao.ishikawa.jp +nomi.ishikawa.jp +nonoichi.ishikawa.jp +noto.ishikawa.jp +shika.ishikawa.jp +suzu.ishikawa.jp +tsubata.ishikawa.jp +tsurugi.ishikawa.jp +uchinada.ishikawa.jp +wajima.ishikawa.jp +fudai.iwate.jp +fujisawa.iwate.jp +hanamaki.iwate.jp +hiraizumi.iwate.jp +hirono.iwate.jp +ichinohe.iwate.jp +ichinoseki.iwate.jp +iwaizumi.iwate.jp +iwate.iwate.jp +joboji.iwate.jp +kamaishi.iwate.jp +kanegasaki.iwate.jp +karumai.iwate.jp +kawai.iwate.jp +kitakami.iwate.jp +kuji.iwate.jp +kunohe.iwate.jp +kuzumaki.iwate.jp +miyako.iwate.jp +mizusawa.iwate.jp +morioka.iwate.jp +ninohe.iwate.jp +noda.iwate.jp +ofunato.iwate.jp +oshu.iwate.jp +otsuchi.iwate.jp +rikuzentakata.iwate.jp +shiwa.iwate.jp +shizukuishi.iwate.jp +sumita.iwate.jp +tanohata.iwate.jp +tono.iwate.jp +yahaba.iwate.jp +yamada.iwate.jp +ayagawa.kagawa.jp +higashikagawa.kagawa.jp +kanonji.kagawa.jp +kotohira.kagawa.jp +manno.kagawa.jp +marugame.kagawa.jp +mitoyo.kagawa.jp +naoshima.kagawa.jp +sanuki.kagawa.jp +tadotsu.kagawa.jp +takamatsu.kagawa.jp +tonosho.kagawa.jp +uchinomi.kagawa.jp +utazu.kagawa.jp +zentsuji.kagawa.jp +akune.kagoshima.jp +amami.kagoshima.jp +hioki.kagoshima.jp +isa.kagoshima.jp +isen.kagoshima.jp +izumi.kagoshima.jp +kagoshima.kagoshima.jp +kanoya.kagoshima.jp +kawanabe.kagoshima.jp +kinko.kagoshima.jp +kouyama.kagoshima.jp +makurazaki.kagoshima.jp +matsumoto.kagoshima.jp +minamitane.kagoshima.jp +nakatane.kagoshima.jp +nishinoomote.kagoshima.jp +satsumasendai.kagoshima.jp +soo.kagoshima.jp +tarumizu.kagoshima.jp +yusui.kagoshima.jp +aikawa.kanagawa.jp +atsugi.kanagawa.jp +ayase.kanagawa.jp +chigasaki.kanagawa.jp +ebina.kanagawa.jp +fujisawa.kanagawa.jp +hadano.kanagawa.jp +hakone.kanagawa.jp +hiratsuka.kanagawa.jp +isehara.kanagawa.jp +kaisei.kanagawa.jp +kamakura.kanagawa.jp +kiyokawa.kanagawa.jp +matsuda.kanagawa.jp +minamiashigara.kanagawa.jp +miura.kanagawa.jp +nakai.kanagawa.jp +ninomiya.kanagawa.jp +odawara.kanagawa.jp +oi.kanagawa.jp +oiso.kanagawa.jp +sagamihara.kanagawa.jp +samukawa.kanagawa.jp +tsukui.kanagawa.jp +yamakita.kanagawa.jp +yamato.kanagawa.jp +yokosuka.kanagawa.jp +yugawara.kanagawa.jp +zama.kanagawa.jp +zushi.kanagawa.jp +aki.kochi.jp +geisei.kochi.jp +hidaka.kochi.jp +higashitsuno.kochi.jp +ino.kochi.jp +kagami.kochi.jp +kami.kochi.jp +kitagawa.kochi.jp +kochi.kochi.jp +mihara.kochi.jp +motoyama.kochi.jp +muroto.kochi.jp +nahari.kochi.jp +nakamura.kochi.jp +nankoku.kochi.jp +nishitosa.kochi.jp +niyodogawa.kochi.jp +ochi.kochi.jp +okawa.kochi.jp +otoyo.kochi.jp +otsuki.kochi.jp +sakawa.kochi.jp +sukumo.kochi.jp +susaki.kochi.jp +tosa.kochi.jp +tosashimizu.kochi.jp +toyo.kochi.jp +tsuno.kochi.jp +umaji.kochi.jp +yasuda.kochi.jp +yusuhara.kochi.jp +amakusa.kumamoto.jp +arao.kumamoto.jp +aso.kumamoto.jp +choyo.kumamoto.jp +gyokuto.kumamoto.jp +kamiamakusa.kumamoto.jp +kikuchi.kumamoto.jp +kumamoto.kumamoto.jp +mashiki.kumamoto.jp +mifune.kumamoto.jp +minamata.kumamoto.jp +minamioguni.kumamoto.jp +nagasu.kumamoto.jp +nishihara.kumamoto.jp +oguni.kumamoto.jp +ozu.kumamoto.jp +sumoto.kumamoto.jp +takamori.kumamoto.jp +uki.kumamoto.jp +uto.kumamoto.jp +yamaga.kumamoto.jp +yamato.kumamoto.jp +yatsushiro.kumamoto.jp +ayabe.kyoto.jp +fukuchiyama.kyoto.jp +higashiyama.kyoto.jp +ide.kyoto.jp +ine.kyoto.jp +joyo.kyoto.jp +kameoka.kyoto.jp +kamo.kyoto.jp +kita.kyoto.jp +kizu.kyoto.jp +kumiyama.kyoto.jp +kyotamba.kyoto.jp +kyotanabe.kyoto.jp +kyotango.kyoto.jp +maizuru.kyoto.jp +minami.kyoto.jp +minamiyamashiro.kyoto.jp +miyazu.kyoto.jp +muko.kyoto.jp +nagaokakyo.kyoto.jp +nakagyo.kyoto.jp +nantan.kyoto.jp +oyamazaki.kyoto.jp +sakyo.kyoto.jp +seika.kyoto.jp +tanabe.kyoto.jp +uji.kyoto.jp +ujitawara.kyoto.jp +wazuka.kyoto.jp +yamashina.kyoto.jp +yawata.kyoto.jp +asahi.mie.jp +inabe.mie.jp +ise.mie.jp +kameyama.mie.jp +kawagoe.mie.jp +kiho.mie.jp +kisosaki.mie.jp +kiwa.mie.jp +komono.mie.jp +kumano.mie.jp +kuwana.mie.jp +matsusaka.mie.jp +meiwa.mie.jp +mihama.mie.jp +minamiise.mie.jp +misugi.mie.jp +miyama.mie.jp +nabari.mie.jp +shima.mie.jp +suzuka.mie.jp +tado.mie.jp +taiki.mie.jp +taki.mie.jp +tamaki.mie.jp +toba.mie.jp +tsu.mie.jp +udono.mie.jp +ureshino.mie.jp +watarai.mie.jp +yokkaichi.mie.jp +furukawa.miyagi.jp +higashimatsushima.miyagi.jp +ishinomaki.miyagi.jp +iwanuma.miyagi.jp +kakuda.miyagi.jp +kami.miyagi.jp +kawasaki.miyagi.jp +marumori.miyagi.jp +matsushima.miyagi.jp +minamisanriku.miyagi.jp +misato.miyagi.jp +murata.miyagi.jp +natori.miyagi.jp +ogawara.miyagi.jp +ohira.miyagi.jp +onagawa.miyagi.jp +osaki.miyagi.jp +rifu.miyagi.jp +semine.miyagi.jp +shibata.miyagi.jp +shichikashuku.miyagi.jp +shikama.miyagi.jp +shiogama.miyagi.jp +shiroishi.miyagi.jp +tagajo.miyagi.jp +taiwa.miyagi.jp +tome.miyagi.jp +tomiya.miyagi.jp +wakuya.miyagi.jp +watari.miyagi.jp +yamamoto.miyagi.jp +zao.miyagi.jp +aya.miyazaki.jp +ebino.miyazaki.jp +gokase.miyazaki.jp +hyuga.miyazaki.jp +kadogawa.miyazaki.jp +kawaminami.miyazaki.jp +kijo.miyazaki.jp +kitagawa.miyazaki.jp +kitakata.miyazaki.jp +kitaura.miyazaki.jp +kobayashi.miyazaki.jp +kunitomi.miyazaki.jp +kushima.miyazaki.jp +mimata.miyazaki.jp +miyakonojo.miyazaki.jp +miyazaki.miyazaki.jp +morotsuka.miyazaki.jp +nichinan.miyazaki.jp +nishimera.miyazaki.jp +nobeoka.miyazaki.jp +saito.miyazaki.jp +shiiba.miyazaki.jp +shintomi.miyazaki.jp +takaharu.miyazaki.jp +takanabe.miyazaki.jp +takazaki.miyazaki.jp +tsuno.miyazaki.jp +achi.nagano.jp +agematsu.nagano.jp +anan.nagano.jp +aoki.nagano.jp +asahi.nagano.jp +azumino.nagano.jp +chikuhoku.nagano.jp +chikuma.nagano.jp +chino.nagano.jp +fujimi.nagano.jp +hakuba.nagano.jp +hara.nagano.jp +hiraya.nagano.jp +iida.nagano.jp +iijima.nagano.jp +iiyama.nagano.jp +iizuna.nagano.jp +ikeda.nagano.jp +ikusaka.nagano.jp +ina.nagano.jp +karuizawa.nagano.jp +kawakami.nagano.jp +kiso.nagano.jp +kisofukushima.nagano.jp +kitaaiki.nagano.jp +komagane.nagano.jp +komoro.nagano.jp +matsukawa.nagano.jp +matsumoto.nagano.jp +miasa.nagano.jp +minamiaiki.nagano.jp +minamimaki.nagano.jp +minamiminowa.nagano.jp +minowa.nagano.jp +miyada.nagano.jp +miyota.nagano.jp +mochizuki.nagano.jp +nagano.nagano.jp +nagawa.nagano.jp +nagiso.nagano.jp +nakagawa.nagano.jp +nakano.nagano.jp +nozawaonsen.nagano.jp +obuse.nagano.jp +ogawa.nagano.jp +okaya.nagano.jp +omachi.nagano.jp +omi.nagano.jp +ookuwa.nagano.jp +ooshika.nagano.jp +otaki.nagano.jp +otari.nagano.jp +sakae.nagano.jp +sakaki.nagano.jp +saku.nagano.jp +sakuho.nagano.jp +shimosuwa.nagano.jp +shinanomachi.nagano.jp +shiojiri.nagano.jp +suwa.nagano.jp +suzaka.nagano.jp +takagi.nagano.jp +takamori.nagano.jp +takayama.nagano.jp +tateshina.nagano.jp +tatsuno.nagano.jp +togakushi.nagano.jp +togura.nagano.jp +tomi.nagano.jp +ueda.nagano.jp +wada.nagano.jp +yamagata.nagano.jp +yamanouchi.nagano.jp +yasaka.nagano.jp +yasuoka.nagano.jp +chijiwa.nagasaki.jp +futsu.nagasaki.jp +goto.nagasaki.jp +hasami.nagasaki.jp +hirado.nagasaki.jp +iki.nagasaki.jp +isahaya.nagasaki.jp +kawatana.nagasaki.jp +kuchinotsu.nagasaki.jp +matsuura.nagasaki.jp +nagasaki.nagasaki.jp +obama.nagasaki.jp +omura.nagasaki.jp +oseto.nagasaki.jp +saikai.nagasaki.jp +sasebo.nagasaki.jp +seihi.nagasaki.jp +shimabara.nagasaki.jp +shinkamigoto.nagasaki.jp +togitsu.nagasaki.jp +tsushima.nagasaki.jp +unzen.nagasaki.jp +ando.nara.jp +gose.nara.jp +heguri.nara.jp +higashiyoshino.nara.jp +ikaruga.nara.jp +ikoma.nara.jp +kamikitayama.nara.jp +kanmaki.nara.jp +kashiba.nara.jp +kashihara.nara.jp +katsuragi.nara.jp +kawai.nara.jp +kawakami.nara.jp +kawanishi.nara.jp +koryo.nara.jp +kurotaki.nara.jp +mitsue.nara.jp +miyake.nara.jp +nara.nara.jp +nosegawa.nara.jp +oji.nara.jp +ouda.nara.jp +oyodo.nara.jp +sakurai.nara.jp +sango.nara.jp +shimoichi.nara.jp +shimokitayama.nara.jp +shinjo.nara.jp +soni.nara.jp +takatori.nara.jp +tawaramoto.nara.jp +tenkawa.nara.jp +tenri.nara.jp +uda.nara.jp +yamatokoriyama.nara.jp +yamatotakada.nara.jp +yamazoe.nara.jp +yoshino.nara.jp +aga.niigata.jp +agano.niigata.jp +gosen.niigata.jp +itoigawa.niigata.jp +izumozaki.niigata.jp +joetsu.niigata.jp +kamo.niigata.jp +kariwa.niigata.jp +kashiwazaki.niigata.jp +minamiuonuma.niigata.jp +mitsuke.niigata.jp +muika.niigata.jp +murakami.niigata.jp +myoko.niigata.jp +nagaoka.niigata.jp +niigata.niigata.jp +ojiya.niigata.jp +omi.niigata.jp +sado.niigata.jp +sanjo.niigata.jp +seiro.niigata.jp +seirou.niigata.jp +sekikawa.niigata.jp +shibata.niigata.jp +tagami.niigata.jp +tainai.niigata.jp +tochio.niigata.jp +tokamachi.niigata.jp +tsubame.niigata.jp +tsunan.niigata.jp +uonuma.niigata.jp +yahiko.niigata.jp +yoita.niigata.jp +yuzawa.niigata.jp +beppu.oita.jp +bungoono.oita.jp +bungotakada.oita.jp +hasama.oita.jp +hiji.oita.jp +himeshima.oita.jp +hita.oita.jp +kamitsue.oita.jp +kokonoe.oita.jp +kuju.oita.jp +kunisaki.oita.jp +kusu.oita.jp +oita.oita.jp +saiki.oita.jp +taketa.oita.jp +tsukumi.oita.jp +usa.oita.jp +usuki.oita.jp +yufu.oita.jp +akaiwa.okayama.jp +asakuchi.okayama.jp +bizen.okayama.jp +hayashima.okayama.jp +ibara.okayama.jp +kagamino.okayama.jp +kasaoka.okayama.jp +kibichuo.okayama.jp +kumenan.okayama.jp +kurashiki.okayama.jp +maniwa.okayama.jp +misaki.okayama.jp +nagi.okayama.jp +niimi.okayama.jp +nishiawakura.okayama.jp +okayama.okayama.jp +satosho.okayama.jp +setouchi.okayama.jp +shinjo.okayama.jp +shoo.okayama.jp +soja.okayama.jp +takahashi.okayama.jp +tamano.okayama.jp +tsuyama.okayama.jp +wake.okayama.jp +yakage.okayama.jp +aguni.okinawa.jp +ginowan.okinawa.jp +ginoza.okinawa.jp +gushikami.okinawa.jp +haebaru.okinawa.jp +higashi.okinawa.jp +hirara.okinawa.jp +iheya.okinawa.jp +ishigaki.okinawa.jp +ishikawa.okinawa.jp +itoman.okinawa.jp +izena.okinawa.jp +kadena.okinawa.jp +kin.okinawa.jp +kitadaito.okinawa.jp +kitanakagusuku.okinawa.jp +kumejima.okinawa.jp +kunigami.okinawa.jp +minamidaito.okinawa.jp +motobu.okinawa.jp +nago.okinawa.jp +naha.okinawa.jp +nakagusuku.okinawa.jp +nakijin.okinawa.jp +nanjo.okinawa.jp +nishihara.okinawa.jp +ogimi.okinawa.jp +okinawa.okinawa.jp +onna.okinawa.jp +shimoji.okinawa.jp +taketomi.okinawa.jp +tarama.okinawa.jp +tokashiki.okinawa.jp +tomigusuku.okinawa.jp +tonaki.okinawa.jp +urasoe.okinawa.jp +uruma.okinawa.jp +yaese.okinawa.jp +yomitan.okinawa.jp +yonabaru.okinawa.jp +yonaguni.okinawa.jp +zamami.okinawa.jp +abeno.osaka.jp +chihayaakasaka.osaka.jp +chuo.osaka.jp +daito.osaka.jp +fujiidera.osaka.jp +habikino.osaka.jp +hannan.osaka.jp +higashiosaka.osaka.jp +higashisumiyoshi.osaka.jp +higashiyodogawa.osaka.jp +hirakata.osaka.jp +ibaraki.osaka.jp +ikeda.osaka.jp +izumi.osaka.jp +izumiotsu.osaka.jp +izumisano.osaka.jp +kadoma.osaka.jp +kaizuka.osaka.jp +kanan.osaka.jp +kashiwara.osaka.jp +katano.osaka.jp +kawachinagano.osaka.jp +kishiwada.osaka.jp +kita.osaka.jp +kumatori.osaka.jp +matsubara.osaka.jp +minato.osaka.jp +minoh.osaka.jp +misaki.osaka.jp +moriguchi.osaka.jp +neyagawa.osaka.jp +nishi.osaka.jp +nose.osaka.jp +osakasayama.osaka.jp +sakai.osaka.jp +sayama.osaka.jp +sennan.osaka.jp +settsu.osaka.jp +shijonawate.osaka.jp +shimamoto.osaka.jp +suita.osaka.jp +tadaoka.osaka.jp +taishi.osaka.jp +tajiri.osaka.jp +takaishi.osaka.jp +takatsuki.osaka.jp +tondabayashi.osaka.jp +toyonaka.osaka.jp +toyono.osaka.jp +yao.osaka.jp +ariake.saga.jp +arita.saga.jp +fukudomi.saga.jp +genkai.saga.jp +hamatama.saga.jp +hizen.saga.jp +imari.saga.jp +kamimine.saga.jp +kanzaki.saga.jp +karatsu.saga.jp +kashima.saga.jp +kitagata.saga.jp +kitahata.saga.jp +kiyama.saga.jp +kouhoku.saga.jp +kyuragi.saga.jp +nishiarita.saga.jp +ogi.saga.jp +omachi.saga.jp +ouchi.saga.jp +saga.saga.jp +shiroishi.saga.jp +taku.saga.jp +tara.saga.jp +tosu.saga.jp +yoshinogari.saga.jp +arakawa.saitama.jp +asaka.saitama.jp +chichibu.saitama.jp +fujimi.saitama.jp +fujimino.saitama.jp +fukaya.saitama.jp +hanno.saitama.jp +hanyu.saitama.jp +hasuda.saitama.jp +hatogaya.saitama.jp +hatoyama.saitama.jp +hidaka.saitama.jp +higashichichibu.saitama.jp +higashimatsuyama.saitama.jp +honjo.saitama.jp +ina.saitama.jp +iruma.saitama.jp +iwatsuki.saitama.jp +kamiizumi.saitama.jp +kamikawa.saitama.jp +kamisato.saitama.jp +kasukabe.saitama.jp +kawagoe.saitama.jp +kawaguchi.saitama.jp +kawajima.saitama.jp +kazo.saitama.jp +kitamoto.saitama.jp +koshigaya.saitama.jp +kounosu.saitama.jp +kuki.saitama.jp +kumagaya.saitama.jp +matsubushi.saitama.jp +minano.saitama.jp +misato.saitama.jp +miyashiro.saitama.jp +miyoshi.saitama.jp +moroyama.saitama.jp +nagatoro.saitama.jp +namegawa.saitama.jp +niiza.saitama.jp +ogano.saitama.jp +ogawa.saitama.jp +ogose.saitama.jp +okegawa.saitama.jp +omiya.saitama.jp +otaki.saitama.jp +ranzan.saitama.jp +ryokami.saitama.jp +saitama.saitama.jp +sakado.saitama.jp +satte.saitama.jp +sayama.saitama.jp +shiki.saitama.jp +shiraoka.saitama.jp +soka.saitama.jp +sugito.saitama.jp +toda.saitama.jp +tokigawa.saitama.jp +tokorozawa.saitama.jp +tsurugashima.saitama.jp +urawa.saitama.jp +warabi.saitama.jp +yashio.saitama.jp +yokoze.saitama.jp +yono.saitama.jp +yorii.saitama.jp +yoshida.saitama.jp +yoshikawa.saitama.jp +yoshimi.saitama.jp +aisho.shiga.jp +gamo.shiga.jp +higashiomi.shiga.jp +hikone.shiga.jp +koka.shiga.jp +konan.shiga.jp +kosei.shiga.jp +koto.shiga.jp +kusatsu.shiga.jp +maibara.shiga.jp +moriyama.shiga.jp +nagahama.shiga.jp +nishiazai.shiga.jp +notogawa.shiga.jp +omihachiman.shiga.jp +otsu.shiga.jp +ritto.shiga.jp +ryuoh.shiga.jp +takashima.shiga.jp +takatsuki.shiga.jp +torahime.shiga.jp +toyosato.shiga.jp +yasu.shiga.jp +akagi.shimane.jp +ama.shimane.jp +gotsu.shimane.jp +hamada.shimane.jp +higashiizumo.shimane.jp +hikawa.shimane.jp +hikimi.shimane.jp +izumo.shimane.jp +kakinoki.shimane.jp +masuda.shimane.jp +matsue.shimane.jp +misato.shimane.jp +nishinoshima.shimane.jp +ohda.shimane.jp +okinoshima.shimane.jp +okuizumo.shimane.jp +shimane.shimane.jp +tamayu.shimane.jp +tsuwano.shimane.jp +unnan.shimane.jp +yakumo.shimane.jp +yasugi.shimane.jp +yatsuka.shimane.jp +arai.shizuoka.jp +atami.shizuoka.jp +fuji.shizuoka.jp +fujieda.shizuoka.jp +fujikawa.shizuoka.jp +fujinomiya.shizuoka.jp +fukuroi.shizuoka.jp +gotemba.shizuoka.jp +haibara.shizuoka.jp +hamamatsu.shizuoka.jp +higashiizu.shizuoka.jp +ito.shizuoka.jp +iwata.shizuoka.jp +izu.shizuoka.jp +izunokuni.shizuoka.jp +kakegawa.shizuoka.jp +kannami.shizuoka.jp +kawanehon.shizuoka.jp +kawazu.shizuoka.jp +kikugawa.shizuoka.jp +kosai.shizuoka.jp +makinohara.shizuoka.jp +matsuzaki.shizuoka.jp +minamiizu.shizuoka.jp +mishima.shizuoka.jp +morimachi.shizuoka.jp +nishiizu.shizuoka.jp +numazu.shizuoka.jp +omaezaki.shizuoka.jp +shimada.shizuoka.jp +shimizu.shizuoka.jp +shimoda.shizuoka.jp +shizuoka.shizuoka.jp +susono.shizuoka.jp +yaizu.shizuoka.jp +yoshida.shizuoka.jp +ashikaga.tochigi.jp +bato.tochigi.jp +haga.tochigi.jp +ichikai.tochigi.jp +iwafune.tochigi.jp +kaminokawa.tochigi.jp +kanuma.tochigi.jp +karasuyama.tochigi.jp +kuroiso.tochigi.jp +mashiko.tochigi.jp +mibu.tochigi.jp +moka.tochigi.jp +motegi.tochigi.jp +nasu.tochigi.jp +nasushiobara.tochigi.jp +nikko.tochigi.jp +nishikata.tochigi.jp +nogi.tochigi.jp +ohira.tochigi.jp +ohtawara.tochigi.jp +oyama.tochigi.jp +sakura.tochigi.jp +sano.tochigi.jp +shimotsuke.tochigi.jp +shioya.tochigi.jp +takanezawa.tochigi.jp +tochigi.tochigi.jp +tsuga.tochigi.jp +ujiie.tochigi.jp +utsunomiya.tochigi.jp +yaita.tochigi.jp +aizumi.tokushima.jp +anan.tokushima.jp +ichiba.tokushima.jp +itano.tokushima.jp +kainan.tokushima.jp +komatsushima.tokushima.jp +matsushige.tokushima.jp +mima.tokushima.jp +minami.tokushima.jp +miyoshi.tokushima.jp +mugi.tokushima.jp +nakagawa.tokushima.jp +naruto.tokushima.jp +sanagochi.tokushima.jp +shishikui.tokushima.jp +tokushima.tokushima.jp +wajiki.tokushima.jp +adachi.tokyo.jp +akiruno.tokyo.jp +akishima.tokyo.jp +aogashima.tokyo.jp +arakawa.tokyo.jp +bunkyo.tokyo.jp +chiyoda.tokyo.jp +chofu.tokyo.jp +chuo.tokyo.jp +edogawa.tokyo.jp +fuchu.tokyo.jp +fussa.tokyo.jp +hachijo.tokyo.jp +hachioji.tokyo.jp +hamura.tokyo.jp +higashikurume.tokyo.jp +higashimurayama.tokyo.jp +higashiyamato.tokyo.jp +hino.tokyo.jp +hinode.tokyo.jp +hinohara.tokyo.jp +inagi.tokyo.jp +itabashi.tokyo.jp +katsushika.tokyo.jp +kita.tokyo.jp +kiyose.tokyo.jp +kodaira.tokyo.jp +koganei.tokyo.jp +kokubunji.tokyo.jp +komae.tokyo.jp +koto.tokyo.jp +kouzushima.tokyo.jp +kunitachi.tokyo.jp +machida.tokyo.jp +meguro.tokyo.jp +minato.tokyo.jp +mitaka.tokyo.jp +mizuho.tokyo.jp +musashimurayama.tokyo.jp +musashino.tokyo.jp +nakano.tokyo.jp +nerima.tokyo.jp +ogasawara.tokyo.jp +okutama.tokyo.jp +ome.tokyo.jp +oshima.tokyo.jp +ota.tokyo.jp +setagaya.tokyo.jp +shibuya.tokyo.jp +shinagawa.tokyo.jp +shinjuku.tokyo.jp +suginami.tokyo.jp +sumida.tokyo.jp +tachikawa.tokyo.jp +taito.tokyo.jp +tama.tokyo.jp +toshima.tokyo.jp +chizu.tottori.jp +hino.tottori.jp +kawahara.tottori.jp +koge.tottori.jp +kotoura.tottori.jp +misasa.tottori.jp +nanbu.tottori.jp +nichinan.tottori.jp +sakaiminato.tottori.jp +tottori.tottori.jp +wakasa.tottori.jp +yazu.tottori.jp +yonago.tottori.jp +asahi.toyama.jp +fuchu.toyama.jp +fukumitsu.toyama.jp +funahashi.toyama.jp +himi.toyama.jp +imizu.toyama.jp +inami.toyama.jp +johana.toyama.jp +kamiichi.toyama.jp +kurobe.toyama.jp +nakaniikawa.toyama.jp +namerikawa.toyama.jp +nanto.toyama.jp +nyuzen.toyama.jp +oyabe.toyama.jp +taira.toyama.jp +takaoka.toyama.jp +tateyama.toyama.jp +toga.toyama.jp +tonami.toyama.jp +toyama.toyama.jp +unazuki.toyama.jp +uozu.toyama.jp +yamada.toyama.jp +arida.wakayama.jp +aridagawa.wakayama.jp +gobo.wakayama.jp +hashimoto.wakayama.jp +hidaka.wakayama.jp +hirogawa.wakayama.jp +inami.wakayama.jp +iwade.wakayama.jp +kainan.wakayama.jp +kamitonda.wakayama.jp +katsuragi.wakayama.jp +kimino.wakayama.jp +kinokawa.wakayama.jp +kitayama.wakayama.jp +koya.wakayama.jp +koza.wakayama.jp +kozagawa.wakayama.jp +kudoyama.wakayama.jp +kushimoto.wakayama.jp +mihama.wakayama.jp +misato.wakayama.jp +nachikatsuura.wakayama.jp +shingu.wakayama.jp +shirahama.wakayama.jp +taiji.wakayama.jp +tanabe.wakayama.jp +wakayama.wakayama.jp +yuasa.wakayama.jp +yura.wakayama.jp +asahi.yamagata.jp +funagata.yamagata.jp +higashine.yamagata.jp +iide.yamagata.jp +kahoku.yamagata.jp +kaminoyama.yamagata.jp +kaneyama.yamagata.jp +kawanishi.yamagata.jp +mamurogawa.yamagata.jp +mikawa.yamagata.jp +murayama.yamagata.jp +nagai.yamagata.jp +nakayama.yamagata.jp +nanyo.yamagata.jp +nishikawa.yamagata.jp +obanazawa.yamagata.jp +oe.yamagata.jp +oguni.yamagata.jp +ohkura.yamagata.jp +oishida.yamagata.jp +sagae.yamagata.jp +sakata.yamagata.jp +sakegawa.yamagata.jp +shinjo.yamagata.jp +shirataka.yamagata.jp +shonai.yamagata.jp +takahata.yamagata.jp +tendo.yamagata.jp +tozawa.yamagata.jp +tsuruoka.yamagata.jp +yamagata.yamagata.jp +yamanobe.yamagata.jp +yonezawa.yamagata.jp +yuza.yamagata.jp +abu.yamaguchi.jp +hagi.yamaguchi.jp +hikari.yamaguchi.jp +hofu.yamaguchi.jp +iwakuni.yamaguchi.jp +kudamatsu.yamaguchi.jp +mitou.yamaguchi.jp +nagato.yamaguchi.jp +oshima.yamaguchi.jp +shimonoseki.yamaguchi.jp +shunan.yamaguchi.jp +tabuse.yamaguchi.jp +tokuyama.yamaguchi.jp +toyota.yamaguchi.jp +ube.yamaguchi.jp +yuu.yamaguchi.jp +chuo.yamanashi.jp +doshi.yamanashi.jp +fuefuki.yamanashi.jp +fujikawa.yamanashi.jp +fujikawaguchiko.yamanashi.jp +fujiyoshida.yamanashi.jp +hayakawa.yamanashi.jp +hokuto.yamanashi.jp +ichikawamisato.yamanashi.jp +kai.yamanashi.jp +kofu.yamanashi.jp +koshu.yamanashi.jp +kosuge.yamanashi.jp +minami-alps.yamanashi.jp +minobu.yamanashi.jp +nakamichi.yamanashi.jp +nanbu.yamanashi.jp +narusawa.yamanashi.jp +nirasaki.yamanashi.jp +nishikatsura.yamanashi.jp +oshino.yamanashi.jp +otsuki.yamanashi.jp +showa.yamanashi.jp +tabayama.yamanashi.jp +tsuru.yamanashi.jp +uenohara.yamanashi.jp +yamanakako.yamanashi.jp +yamanashi.yamanashi.jp + +// ke : http://www.kenic.or.ke/index.php/en/ke-domains/ke-domains +ke +ac.ke +co.ke +go.ke +info.ke +me.ke +mobi.ke +ne.ke +or.ke +sc.ke + +// kg : http://www.domain.kg/dmn_n.html +kg +com.kg +edu.kg +gov.kg +mil.kg +net.kg +org.kg + +// kh : http://www.mptc.gov.kh/dns_registration.htm +*.kh + +// ki : https://www.iana.org/domains/root/db/ki.html +ki +biz.ki +com.ki +edu.ki +gov.ki +info.ki +net.ki +org.ki + +// km : https://www.iana.org/domains/root/db/km.html +// http://www.domaine.km/documents/charte.doc +km +ass.km +com.km +edu.km +gov.km +mil.km +nom.km +org.km +prd.km +tm.km +// These are only mentioned as proposed suggestions at domaine.km, but +// https://www.iana.org/domains/root/db/km.html says they're available for registration: +asso.km +coop.km +gouv.km +medecin.km +notaires.km +pharmaciens.km +presse.km +veterinaire.km + +// kn : https://www.iana.org/domains/root/db/kn.html +// http://www.dot.kn/domainRules.html +kn +edu.kn +gov.kn +net.kn +org.kn + +// kp : http://www.kcce.kp/en_index.php +kp +com.kp +edu.kp +gov.kp +org.kp +rep.kp +tra.kp + +// kr : https://www.iana.org/domains/root/db/kr.html +// see also: https://krnic.kisa.or.kr/jsp/infoboard/law/domBylawsReg.jsp +kr +ac.kr +ai.kr +co.kr +es.kr +go.kr +hs.kr +io.kr +it.kr +kg.kr +me.kr +mil.kr +ms.kr +ne.kr +or.kr +pe.kr +re.kr +sc.kr +// kr geographical names +busan.kr +chungbuk.kr +chungnam.kr +daegu.kr +daejeon.kr +gangwon.kr +gwangju.kr +gyeongbuk.kr +gyeonggi.kr +gyeongnam.kr +incheon.kr +jeju.kr +jeonbuk.kr +jeonnam.kr +seoul.kr +ulsan.kr + +// kw : https://www.nic.kw/policies/ +// Confirmed by registry +kw +com.kw +edu.kw +emb.kw +gov.kw +ind.kw +net.kw +org.kw + +// ky : http://www.icta.ky/da_ky_reg_dom.php +// Confirmed by registry 2008-06-17 +ky +com.ky +edu.ky +net.ky +org.ky + +// kz : https://www.iana.org/domains/root/db/kz.html +// see also: http://www.nic.kz/rules/index.jsp +kz +com.kz +edu.kz +gov.kz +mil.kz +net.kz +org.kz + +// la : https://www.iana.org/domains/root/db/la.html +// Submitted by registry +la +com.la +edu.la +gov.la +info.la +int.la +net.la +org.la +per.la + +// lb : https://www.iana.org/domains/root/db/lb.html +// Submitted by registry +lb +com.lb +edu.lb +gov.lb +net.lb +org.lb + +// lc : https://www.iana.org/domains/root/db/lc.html +// see also: http://www.nic.lc/rules.htm +lc +co.lc +com.lc +edu.lc +gov.lc +net.lc +org.lc + +// li : https://www.iana.org/domains/root/db/li.html +li + +// lk : https://www.iana.org/domains/root/db/lk.html +lk +ac.lk +assn.lk +com.lk +edu.lk +gov.lk +grp.lk +hotel.lk +int.lk +ltd.lk +net.lk +ngo.lk +org.lk +sch.lk +soc.lk +web.lk + +// lr : http://psg.com/dns/lr/lr.txt +// Submitted by registry +lr +com.lr +edu.lr +gov.lr +net.lr +org.lr + +// ls : http://www.nic.ls/ +// Confirmed by registry +ls +ac.ls +biz.ls +co.ls +edu.ls +gov.ls +info.ls +net.ls +org.ls +sc.ls + +// lt : https://www.iana.org/domains/root/db/lt.html +lt +// gov.lt : http://www.gov.lt/index_en.php +gov.lt + +// lu : http://www.dns.lu/en/ +lu + +// lv : https://www.iana.org/domains/root/db/lv.html +lv +asn.lv +com.lv +conf.lv +edu.lv +gov.lv +id.lv +mil.lv +net.lv +org.lv + +// ly : http://www.nic.ly/regulations.php +ly +com.ly +edu.ly +gov.ly +id.ly +med.ly +net.ly +org.ly +plc.ly +sch.ly + +// ma : https://www.iana.org/domains/root/db/ma.html +// http://www.anrt.ma/fr/admin/download/upload/file_fr782.pdf +ma +ac.ma +co.ma +gov.ma +net.ma +org.ma +press.ma + +// mc : http://www.nic.mc/ +mc +asso.mc +tm.mc + +// md : https://www.iana.org/domains/root/db/md.html +md + +// me : https://www.iana.org/domains/root/db/me.html +me +ac.me +co.me +edu.me +gov.me +its.me +net.me +org.me +priv.me + +// mg : https://nic.mg +mg +co.mg +com.mg +edu.mg +gov.mg +mil.mg +nom.mg +org.mg +prd.mg + +// mh : https://www.iana.org/domains/root/db/mh.html +mh + +// mil : https://www.iana.org/domains/root/db/mil.html +mil + +// mk : https://www.iana.org/domains/root/db/mk.html +// see also: http://dns.marnet.net.mk/postapka.php +mk +com.mk +edu.mk +gov.mk +inf.mk +name.mk +net.mk +org.mk + +// ml : https://www.iana.org/domains/root/db/ml.html +// Confirmed by Boubacar NDIAYE 2024-12-31 +ml +ac.ml +art.ml +asso.ml +com.ml +edu.ml +gouv.ml +gov.ml +info.ml +inst.ml +net.ml +org.ml +pr.ml +presse.ml + +// mm : https://www.iana.org/domains/root/db/mm.html +*.mm + +// mn : https://www.iana.org/domains/root/db/mn.html +mn +edu.mn +gov.mn +org.mn + +// mo : http://www.monic.net.mo/ +mo +com.mo +edu.mo +gov.mo +net.mo +org.mo + +// mobi : https://www.iana.org/domains/root/db/mobi.html +mobi + +// mp : http://www.dot.mp/ +// Confirmed by registry 2008-06-17 +mp + +// mq : https://www.iana.org/domains/root/db/mq.html +mq + +// mr : https://www.iana.org/domains/root/db/mr.html +mr +gov.mr + +// ms : https://www.iana.org/domains/root/db/ms.html +ms +com.ms +edu.ms +gov.ms +net.ms +org.ms + +// mt : https://www.nic.org.mt/go/policy +// Submitted by registry +mt +com.mt +edu.mt +net.mt +org.mt + +// mu : https://www.iana.org/domains/root/db/mu.html +mu +ac.mu +co.mu +com.mu +gov.mu +net.mu +or.mu +org.mu + +// museum : https://welcome.museum/wp-content/uploads/2018/05/20180525-Registration-Policy-MUSEUM-EN_VF-2.pdf https://welcome.museum/buy-your-dot-museum-2/ +museum + +// mv : https://www.iana.org/domains/root/db/mv.html +// "mv" included because, contra Wikipedia, google.mv exists. +mv +aero.mv +biz.mv +com.mv +coop.mv +edu.mv +gov.mv +info.mv +int.mv +mil.mv +museum.mv +name.mv +net.mv +org.mv +pro.mv + +// mw : http://www.registrar.mw/ +mw +ac.mw +biz.mw +co.mw +com.mw +coop.mw +edu.mw +gov.mw +int.mw +net.mw +org.mw + +// mx : http://www.nic.mx/ +// Submitted by registry +mx +com.mx +edu.mx +gob.mx +net.mx +org.mx + +// my : http://www.mynic.my/ +// Available strings: https://mynic.my/resources/domains/buying-a-domain/ +my +biz.my +com.my +edu.my +gov.my +mil.my +name.my +net.my +org.my + +// mz : http://www.uem.mz/ +// Submitted by registry +mz +ac.mz +adv.mz +co.mz +edu.mz +gov.mz +mil.mz +net.mz +org.mz + +// na : http://www.na-nic.com.na/ +na +alt.na +co.na +com.na +gov.na +net.na +org.na + +// name : http://www.nic.name/ +// Regarding 2LDs: https://github.com/publicsuffix/list/issues/2306 +name + +// nc : http://www.cctld.nc/ +nc +asso.nc +nom.nc + +// ne : https://www.iana.org/domains/root/db/ne.html +ne + +// net : https://www.iana.org/domains/root/db/net.html +net + +// nf : https://www.iana.org/domains/root/db/nf.html +nf +arts.nf +com.nf +firm.nf +info.nf +net.nf +other.nf +per.nf +rec.nf +store.nf +web.nf + +// ng : http://www.nira.org.ng/index.php/join-us/register-ng-domain/189-nira-slds +ng +com.ng +edu.ng +gov.ng +i.ng +mil.ng +mobi.ng +name.ng +net.ng +org.ng +sch.ng + +// ni : http://www.nic.ni/ +ni +ac.ni +biz.ni +co.ni +com.ni +edu.ni +gob.ni +in.ni +info.ni +int.ni +mil.ni +net.ni +nom.ni +org.ni +web.ni + +// nl : https://www.iana.org/domains/root/db/nl.html +// https://www.sidn.nl/ +nl + +// no : https://www.norid.no/en/om-domenenavn/regelverk-for-no/ +// Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/ +// Norid category second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-c/ +// Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/ +// RSS feed: https://teknisk.norid.no/en/feed/ +no +// Norid category second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-c/ +fhs.no +folkebibl.no +fylkesbibl.no +idrett.no +museum.no +priv.no +vgs.no +// Norid category second-level domains managed by parties other than Norid : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-d/ +dep.no +herad.no +kommune.no +mil.no +stat.no +// Norid geographical second level domains : https://www.norid.no/en/om-domenenavn/regelverk-for-no/vedlegg-b/ +// counties +aa.no +ah.no +bu.no +fm.no +hl.no +hm.no +jan-mayen.no +mr.no +nl.no +nt.no +of.no +ol.no +oslo.no +rl.no +sf.no +st.no +svalbard.no +tm.no +tr.no +va.no +vf.no +// primary and lower secondary schools per county +gs.aa.no +gs.ah.no +gs.bu.no +gs.fm.no +gs.hl.no +gs.hm.no +gs.jan-mayen.no +gs.mr.no +gs.nl.no +gs.nt.no +gs.of.no +gs.ol.no +gs.oslo.no +gs.rl.no +gs.sf.no +gs.st.no +gs.svalbard.no +gs.tm.no +gs.tr.no +gs.va.no +gs.vf.no +// cities +akrehamn.no +åkrehamn.no +algard.no +ålgård.no +arna.no +bronnoysund.no +brønnøysund.no +brumunddal.no +bryne.no +drobak.no +drøbak.no +egersund.no +fetsund.no +floro.no +florø.no +fredrikstad.no +hokksund.no +honefoss.no +hønefoss.no +jessheim.no +jorpeland.no +jørpeland.no +kirkenes.no +kopervik.no +krokstadelva.no +langevag.no +langevåg.no +leirvik.no +mjondalen.no +mjøndalen.no +mo-i-rana.no +mosjoen.no +mosjøen.no +nesoddtangen.no +orkanger.no +osoyro.no +osøyro.no +raholt.no +råholt.no +sandnessjoen.no +sandnessjøen.no +skedsmokorset.no +slattum.no +spjelkavik.no +stathelle.no +stavern.no +stjordalshalsen.no +stjørdalshalsen.no +tananger.no +tranby.no +vossevangen.no +// communities +aarborte.no +aejrie.no +afjord.no +åfjord.no +agdenes.no +nes.akershus.no +aknoluokta.no +ákŋoluokta.no +al.no +ål.no +alaheadju.no +álaheadju.no +alesund.no +ålesund.no +alstahaug.no +alta.no +áltá.no +alvdal.no +amli.no +åmli.no +amot.no +åmot.no +andasuolo.no +andebu.no +andoy.no +andøy.no +ardal.no +årdal.no +aremark.no +arendal.no +ås.no +aseral.no +åseral.no +asker.no +askim.no +askoy.no +askøy.no +askvoll.no +asnes.no +åsnes.no +audnedaln.no +aukra.no +aure.no +aurland.no +aurskog-holand.no +aurskog-høland.no +austevoll.no +austrheim.no +averoy.no +averøy.no +badaddja.no +bådåddjå.no +bærum.no +bahcavuotna.no +báhcavuotna.no +bahccavuotna.no +báhccavuotna.no +baidar.no +báidár.no +bajddar.no +bájddar.no +balat.no +bálát.no +balestrand.no +ballangen.no +balsfjord.no +bamble.no +bardu.no +barum.no +batsfjord.no +båtsfjord.no +bearalvahki.no +bearalváhki.no +beardu.no +beiarn.no +berg.no +bergen.no +berlevag.no +berlevåg.no +bievat.no +bievát.no +bindal.no +birkenes.no +bjerkreim.no +bjugn.no +bodo.no +bodø.no +bokn.no +bomlo.no +bømlo.no +bremanger.no +bronnoy.no +brønnøy.no +budejju.no +nes.buskerud.no +bygland.no +bykle.no +cahcesuolo.no +čáhcesuolo.no +davvenjarga.no +davvenjárga.no +davvesiida.no +deatnu.no +dielddanuorri.no +divtasvuodna.no +divttasvuotna.no +donna.no +dønna.no +dovre.no +drammen.no +drangedal.no +dyroy.no +dyrøy.no +eid.no +eidfjord.no +eidsberg.no +eidskog.no +eidsvoll.no +eigersund.no +elverum.no +enebakk.no +engerdal.no +etne.no +etnedal.no +evenassi.no +evenášši.no +evenes.no +evje-og-hornnes.no +farsund.no +fauske.no +fedje.no +fet.no +finnoy.no +finnøy.no +fitjar.no +fjaler.no +fjell.no +fla.no +flå.no +flakstad.no +flatanger.no +flekkefjord.no +flesberg.no +flora.no +folldal.no +forde.no +førde.no +forsand.no +fosnes.no +fræna.no +frana.no +frei.no +frogn.no +froland.no +frosta.no +froya.no +frøya.no +fuoisku.no +fuossko.no +fusa.no +fyresdal.no +gaivuotna.no +gáivuotna.no +galsa.no +gálsá.no +gamvik.no +gangaviika.no +gáŋgaviika.no +gaular.no +gausdal.no +giehtavuoatna.no +gildeskal.no +gildeskål.no +giske.no +gjemnes.no +gjerdrum.no +gjerstad.no +gjesdal.no +gjovik.no +gjøvik.no +gloppen.no +gol.no +gran.no +grane.no +granvin.no +gratangen.no +grimstad.no +grong.no +grue.no +gulen.no +guovdageaidnu.no +ha.no +hå.no +habmer.no +hábmer.no +hadsel.no +hægebostad.no +hagebostad.no +halden.no +halsa.no +hamar.no +hamaroy.no +hammarfeasta.no +hámmárfeasta.no +hammerfest.no +hapmir.no +hápmir.no +haram.no +hareid.no +harstad.no +hasvik.no +hattfjelldal.no +haugesund.no +os.hedmark.no +valer.hedmark.no +våler.hedmark.no +hemne.no +hemnes.no +hemsedal.no +hitra.no +hjartdal.no +hjelmeland.no +hobol.no +hobøl.no +hof.no +hol.no +hole.no +holmestrand.no +holtalen.no +holtålen.no +os.hordaland.no +hornindal.no +horten.no +hoyanger.no +høyanger.no +hoylandet.no +høylandet.no +hurdal.no +hurum.no +hvaler.no +hyllestad.no +ibestad.no +inderoy.no +inderøy.no +iveland.no +ivgu.no +jevnaker.no +jolster.no +jølster.no +jondal.no +kafjord.no +kåfjord.no +karasjohka.no +kárášjohka.no +karasjok.no +karlsoy.no +karmoy.no +karmøy.no +kautokeino.no +klabu.no +klæbu.no +klepp.no +kongsberg.no +kongsvinger.no +kraanghke.no +kråanghke.no +kragero.no +kragerø.no +kristiansand.no +kristiansund.no +krodsherad.no +krødsherad.no +kvæfjord.no +kvænangen.no +kvafjord.no +kvalsund.no +kvam.no +kvanangen.no +kvinesdal.no +kvinnherad.no +kviteseid.no +kvitsoy.no +kvitsøy.no +laakesvuemie.no +lærdal.no +lahppi.no +láhppi.no +lardal.no +larvik.no +lavagis.no +lavangen.no +leangaviika.no +leaŋgaviika.no +lebesby.no +leikanger.no +leirfjord.no +leka.no +leksvik.no +lenvik.no +lerdal.no +lesja.no +levanger.no +lier.no +lierne.no +lillehammer.no +lillesand.no +lindas.no +lindås.no +lindesnes.no +loabat.no +loabát.no +lodingen.no +lødingen.no +lom.no +loppa.no +lorenskog.no +lørenskog.no +loten.no +løten.no +lund.no +lunner.no +luroy.no +lurøy.no +luster.no +lyngdal.no +lyngen.no +malatvuopmi.no +málatvuopmi.no +malselv.no +målselv.no +malvik.no +mandal.no +marker.no +marnardal.no +masfjorden.no +masoy.no +måsøy.no +matta-varjjat.no +mátta-várjjat.no +meland.no +meldal.no +melhus.no +meloy.no +meløy.no +meraker.no +meråker.no +midsund.no +midtre-gauldal.no +moareke.no +moåreke.no +modalen.no +modum.no +molde.no +heroy.more-og-romsdal.no +sande.more-og-romsdal.no +herøy.møre-og-romsdal.no +sande.møre-og-romsdal.no +moskenes.no +moss.no +muosat.no +muosát.no +naamesjevuemie.no +nååmesjevuemie.no +nærøy.no +namdalseid.no +namsos.no +namsskogan.no +nannestad.no +naroy.no +narviika.no +narvik.no +naustdal.no +navuotna.no +návuotna.no +nedre-eiker.no +nesna.no +nesodden.no +nesseby.no +nesset.no +nissedal.no +nittedal.no +nord-aurdal.no +nord-fron.no +nord-odal.no +norddal.no +nordkapp.no +bo.nordland.no +bø.nordland.no +heroy.nordland.no +herøy.nordland.no +nordre-land.no +nordreisa.no +nore-og-uvdal.no +notodden.no +notteroy.no +nøtterøy.no +odda.no +oksnes.no +øksnes.no +omasvuotna.no +oppdal.no +oppegard.no +oppegård.no +orkdal.no +orland.no +ørland.no +orskog.no +ørskog.no +orsta.no +ørsta.no +osen.no +osteroy.no +osterøy.no +valer.ostfold.no +våler.østfold.no +ostre-toten.no +østre-toten.no +overhalla.no +ovre-eiker.no +øvre-eiker.no +oyer.no +øyer.no +oygarden.no +øygarden.no +oystre-slidre.no +øystre-slidre.no +porsanger.no +porsangu.no +porsáŋgu.no +porsgrunn.no +rade.no +råde.no +radoy.no +radøy.no +rælingen.no +rahkkeravju.no +ráhkkerávju.no +raisa.no +ráisa.no +rakkestad.no +ralingen.no +rana.no +randaberg.no +rauma.no +rendalen.no +rennebu.no +rennesoy.no +rennesøy.no +rindal.no +ringebu.no +ringerike.no +ringsaker.no +risor.no +risør.no +rissa.no +roan.no +rodoy.no +rødøy.no +rollag.no +romsa.no +romskog.no +rømskog.no +roros.no +røros.no +rost.no +røst.no +royken.no +røyken.no +royrvik.no +røyrvik.no +ruovat.no +rygge.no +salangen.no +salat.no +sálat.no +sálát.no +saltdal.no +samnanger.no +sandefjord.no +sandnes.no +sandoy.no +sandøy.no +sarpsborg.no +sauda.no +sauherad.no +sel.no +selbu.no +selje.no +seljord.no +siellak.no +sigdal.no +siljan.no +sirdal.no +skanit.no +skánit.no +skanland.no +skånland.no +skaun.no +skedsmo.no +ski.no +skien.no +skierva.no +skiervá.no +skiptvet.no +skjak.no +skjåk.no +skjervoy.no +skjervøy.no +skodje.no +smola.no +smøla.no +snaase.no +snåase.no +snasa.no +snåsa.no +snillfjord.no +snoasa.no +sogndal.no +sogne.no +søgne.no +sokndal.no +sola.no +solund.no +somna.no +sømna.no +sondre-land.no +søndre-land.no +songdalen.no +sor-aurdal.no +sør-aurdal.no +sor-fron.no +sør-fron.no +sor-odal.no +sør-odal.no +sor-varanger.no +sør-varanger.no +sorfold.no +sørfold.no +sorreisa.no +sørreisa.no +sortland.no +sorum.no +sørum.no +spydeberg.no +stange.no +stavanger.no +steigen.no +steinkjer.no +stjordal.no +stjørdal.no +stokke.no +stor-elvdal.no +stord.no +stordal.no +storfjord.no +strand.no +stranda.no +stryn.no +sula.no +suldal.no +sund.no +sunndal.no +surnadal.no +sveio.no +svelvik.no +sykkylven.no +tana.no +bo.telemark.no +bø.telemark.no +time.no +tingvoll.no +tinn.no +tjeldsund.no +tjome.no +tjøme.no +tokke.no +tolga.no +tonsberg.no +tønsberg.no +torsken.no +træna.no +trana.no +tranoy.no +tranøy.no +troandin.no +trogstad.no +trøgstad.no +tromsa.no +tromso.no +tromsø.no +trondheim.no +trysil.no +tvedestrand.no +tydal.no +tynset.no +tysfjord.no +tysnes.no +tysvær.no +tysvar.no +ullensaker.no +ullensvang.no +ulvik.no +unjarga.no +unjárga.no +utsira.no +vaapste.no +vadso.no +vadsø.no +værøy.no +vaga.no +vågå.no +vagan.no +vågan.no +vagsoy.no +vågsøy.no +vaksdal.no +valle.no +vang.no +vanylven.no +vardo.no +vardø.no +varggat.no +várggát.no +varoy.no +vefsn.no +vega.no +vegarshei.no +vegårshei.no +vennesla.no +verdal.no +verran.no +vestby.no +sande.vestfold.no +vestnes.no +vestre-slidre.no +vestre-toten.no +vestvagoy.no +vestvågøy.no +vevelstad.no +vik.no +vikna.no +vindafjord.no +voagat.no +volda.no +voss.no + +// np : http://www.mos.com.np/register.html +*.np + +// nr : http://cenpac.net.nr/dns/index.html +// Submitted by registry +nr +biz.nr +com.nr +edu.nr +gov.nr +info.nr +net.nr +org.nr + +// nu : https://www.iana.org/domains/root/db/nu.html +nu + +// nz : https://www.iana.org/domains/root/db/nz.html +// Submitted by registry +nz +ac.nz +co.nz +cri.nz +geek.nz +gen.nz +govt.nz +health.nz +iwi.nz +kiwi.nz +maori.nz +māori.nz +mil.nz +net.nz +org.nz +parliament.nz +school.nz + +// om : https://www.iana.org/domains/root/db/om.html +om +co.om +com.om +edu.om +gov.om +med.om +museum.om +net.om +org.om +pro.om + +// onion : https://tools.ietf.org/html/rfc7686 +onion + +// org : https://www.iana.org/domains/root/db/org.html +org + +// pa : http://www.nic.pa/ +// Some additional second level "domains" resolve directly as hostnames, such as +// pannet.pa, so we add a rule for "pa". +pa +abo.pa +ac.pa +com.pa +edu.pa +gob.pa +ing.pa +med.pa +net.pa +nom.pa +org.pa +sld.pa + +// pe : https://www.nic.pe/InformeFinalComision.pdf +pe +com.pe +edu.pe +gob.pe +mil.pe +net.pe +nom.pe +org.pe + +// pf : http://www.gobin.info/domainname/formulaire-pf.pdf +pf +com.pf +edu.pf +org.pf + +// pg : https://www.iana.org/domains/root/db/pg.html +*.pg + +// ph : https://www.iana.org/domains/root/db/ph.html +// Submitted by registry +ph +com.ph +edu.ph +gov.ph +i.ph +mil.ph +net.ph +ngo.ph +org.ph + +// pk : https://pk5.pknic.net.pk/pk5/msgNamepk.PK +// Contact Email: staff@pknic.net.pk +pk +ac.pk +biz.pk +com.pk +edu.pk +fam.pk +gkp.pk +gob.pk +gog.pk +gok.pk +gop.pk +gos.pk +gov.pk +net.pk +org.pk +web.pk + +// pl : https://www.dns.pl/en/ +// Confirmed by registry 2024-11-18 +pl +com.pl +net.pl +org.pl +// pl functional domains : https://www.dns.pl/en/list_of_functional_domain_names +agro.pl +aid.pl +atm.pl +auto.pl +biz.pl +edu.pl +gmina.pl +gsm.pl +info.pl +mail.pl +media.pl +miasta.pl +mil.pl +nieruchomosci.pl +nom.pl +pc.pl +powiat.pl +priv.pl +realestate.pl +rel.pl +sex.pl +shop.pl +sklep.pl +sos.pl +szkola.pl +targi.pl +tm.pl +tourism.pl +travel.pl +turystyka.pl +// Government domains : https://www.dns.pl/informacje_o_rejestracji_domen_gov_pl +// In accordance with the .gov.pl Domain Name Regulations : https://www.dns.pl/regulamin_gov_pl +gov.pl +ap.gov.pl +griw.gov.pl +ic.gov.pl +is.gov.pl +kmpsp.gov.pl +konsulat.gov.pl +kppsp.gov.pl +kwp.gov.pl +kwpsp.gov.pl +mup.gov.pl +mw.gov.pl +oia.gov.pl +oirm.gov.pl +oke.gov.pl +oow.gov.pl +oschr.gov.pl +oum.gov.pl +pa.gov.pl +pinb.gov.pl +piw.gov.pl +po.gov.pl +pr.gov.pl +psp.gov.pl +psse.gov.pl +pup.gov.pl +rzgw.gov.pl +sa.gov.pl +sdn.gov.pl +sko.gov.pl +so.gov.pl +sr.gov.pl +starostwo.gov.pl +ug.gov.pl +ugim.gov.pl +um.gov.pl +umig.gov.pl +upow.gov.pl +uppo.gov.pl +us.gov.pl +uw.gov.pl +uzs.gov.pl +wif.gov.pl +wiih.gov.pl +winb.gov.pl +wios.gov.pl +witd.gov.pl +wiw.gov.pl +wkz.gov.pl +wsa.gov.pl +wskr.gov.pl +wsse.gov.pl +wuoz.gov.pl +wzmiuw.gov.pl +zp.gov.pl +zpisdn.gov.pl +// pl regional domains : https://www.dns.pl/en/list_of_regional_domain_names +augustow.pl +babia-gora.pl +bedzin.pl +beskidy.pl +bialowieza.pl +bialystok.pl +bielawa.pl +bieszczady.pl +boleslawiec.pl +bydgoszcz.pl +bytom.pl +cieszyn.pl +czeladz.pl +czest.pl +dlugoleka.pl +elblag.pl +elk.pl +glogow.pl +gniezno.pl +gorlice.pl +grajewo.pl +ilawa.pl +jaworzno.pl +jelenia-gora.pl +jgora.pl +kalisz.pl +karpacz.pl +kartuzy.pl +kaszuby.pl +katowice.pl +kazimierz-dolny.pl +kepno.pl +ketrzyn.pl +klodzko.pl +kobierzyce.pl +kolobrzeg.pl +konin.pl +konskowola.pl +kutno.pl +lapy.pl +lebork.pl +legnica.pl +lezajsk.pl +limanowa.pl +lomza.pl +lowicz.pl +lubin.pl +lukow.pl +malbork.pl +malopolska.pl +mazowsze.pl +mazury.pl +mielec.pl +mielno.pl +mragowo.pl +naklo.pl +nowaruda.pl +nysa.pl +olawa.pl +olecko.pl +olkusz.pl +olsztyn.pl +opoczno.pl +opole.pl +ostroda.pl +ostroleka.pl +ostrowiec.pl +ostrowwlkp.pl +pila.pl +pisz.pl +podhale.pl +podlasie.pl +polkowice.pl +pomorskie.pl +pomorze.pl +prochowice.pl +pruszkow.pl +przeworsk.pl +pulawy.pl +radom.pl +rawa-maz.pl +rybnik.pl +rzeszow.pl +sanok.pl +sejny.pl +skoczow.pl +slask.pl +slupsk.pl +sosnowiec.pl +stalowa-wola.pl +starachowice.pl +stargard.pl +suwalki.pl +swidnica.pl +swiebodzin.pl +swinoujscie.pl +szczecin.pl +szczytno.pl +tarnobrzeg.pl +tgory.pl +turek.pl +tychy.pl +ustka.pl +walbrzych.pl +warmia.pl +warszawa.pl +waw.pl +wegrow.pl +wielun.pl +wlocl.pl +wloclawek.pl +wodzislaw.pl +wolomin.pl +wroclaw.pl +zachpomor.pl +zagan.pl +zarow.pl +zgora.pl +zgorzelec.pl + +// pm : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +pm + +// pn : https://www.iana.org/domains/root/db/pn.html +pn +co.pn +edu.pn +gov.pn +net.pn +org.pn + +// post : https://www.iana.org/domains/root/db/post.html +post + +// pr : http://www.nic.pr/index.asp?f=1 +pr +biz.pr +com.pr +edu.pr +gov.pr +info.pr +isla.pr +name.pr +net.pr +org.pr +pro.pr +// these aren't mentioned on nic.pr, but on https://www.iana.org/domains/root/db/pr.html +ac.pr +est.pr +prof.pr + +// pro : http://registry.pro/get-pro +pro +aaa.pro +aca.pro +acct.pro +avocat.pro +bar.pro +cpa.pro +eng.pro +jur.pro +law.pro +med.pro +recht.pro + +// ps : https://www.iana.org/domains/root/db/ps.html +// http://www.nic.ps/registration/policy.html#reg +ps +com.ps +edu.ps +gov.ps +net.ps +org.ps +plo.ps +sec.ps + +// pt : https://www.dns.pt/en/domain/pt-terms-and-conditions-registration-rules/ +pt +com.pt +edu.pt +gov.pt +int.pt +net.pt +nome.pt +org.pt +publ.pt + +// pw : https://www.iana.org/domains/root/db/pw.html +// Confirmed by registry in private correspondence with @dnsguru 2024-12-09 +pw +gov.pw + +// py : https://www.iana.org/domains/root/db/py.html +// Submitted by registry +py +com.py +coop.py +edu.py +gov.py +mil.py +net.py +org.py + +// qa : http://domains.qa/en/ +qa +com.qa +edu.qa +gov.qa +mil.qa +name.qa +net.qa +org.qa +sch.qa + +// re : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +// Confirmed by registry 2024-11-18 +re +// Closed for registration on 2013-03-15 but domains are still maintained +asso.re +com.re + +// ro : http://www.rotld.ro/ +ro +arts.ro +com.ro +firm.ro +info.ro +nom.ro +nt.ro +org.ro +rec.ro +store.ro +tm.ro +www.ro + +// rs : https://www.rnids.rs/en/domains/national-domains +rs +ac.rs +co.rs +edu.rs +gov.rs +in.rs +org.rs + +// ru : https://cctld.ru/files/pdf/docs/en/rules_ru-rf.pdf +// Submitted by George Georgievsky +ru + +// rw : https://www.iana.org/domains/root/db/rw.html +rw +ac.rw +co.rw +coop.rw +gov.rw +mil.rw +net.rw +org.rw + +// sa : http://www.nic.net.sa/ +sa +com.sa +edu.sa +gov.sa +med.sa +net.sa +org.sa +pub.sa +sch.sa + +// sb : http://www.sbnic.net.sb/ +// Submitted by registry +sb +com.sb +edu.sb +gov.sb +net.sb +org.sb + +// sc : http://www.nic.sc/ +sc +com.sc +edu.sc +gov.sc +net.sc +org.sc + +// sd : https://www.iana.org/domains/root/db/sd.html +// Submitted by registry +sd +com.sd +edu.sd +gov.sd +info.sd +med.sd +net.sd +org.sd +tv.sd + +// se : https://www.iana.org/domains/root/db/se.html +// https://data.internetstiftelsen.se/barred_domains_list.txt -> Second level domains & Sub-domains +// Confirmed by Registry Services 2024-11-20 +se +a.se +ac.se +b.se +bd.se +brand.se +c.se +d.se +e.se +f.se +fh.se +fhsk.se +fhv.se +g.se +h.se +i.se +k.se +komforb.se +kommunalforbund.se +komvux.se +l.se +lanbib.se +m.se +n.se +naturbruksgymn.se +o.se +org.se +p.se +parti.se +pp.se +press.se +r.se +s.se +t.se +tm.se +u.se +w.se +x.se +y.se +z.se + +// sg : https://www.sgnic.sg/domain-registration/sg-categories-rules +// Confirmed by registry 2024-11-19 +sg +com.sg +edu.sg +gov.sg +net.sg +org.sg + +// sh : http://nic.sh/rules.htm +sh +com.sh +gov.sh +mil.sh +net.sh +org.sh + +// si : https://www.iana.org/domains/root/db/si.html +si + +// sj : No registrations at this time. +// Submitted by registry +sj + +// sk : https://www.iana.org/domains/root/db/sk.html +// https://sk-nic.sk/ +sk +org.sk + +// sl : http://www.nic.sl +// Submitted by registry +sl +com.sl +edu.sl +gov.sl +net.sl +org.sl + +// sm : https://www.iana.org/domains/root/db/sm.html +sm + +// sn : https://www.iana.org/domains/root/db/sn.html +sn +art.sn +com.sn +edu.sn +gouv.sn +org.sn +univ.sn + +// so : http://sonic.so/policies/ +so +com.so +edu.so +gov.so +me.so +net.so +org.so + +// sr : https://www.iana.org/domains/root/db/sr.html +sr + +// ss : https://registry.nic.ss/ +// Submitted by registry +ss +biz.ss +co.ss +com.ss +edu.ss +gov.ss +me.ss +net.ss +org.ss +sch.ss + +// st : http://www.nic.st/html/policyrules/ +st +co.st +com.st +consulado.st +edu.st +embaixada.st +mil.st +net.st +org.st +principe.st +saotome.st +store.st + +// su : https://www.iana.org/domains/root/db/su.html +su + +// sv : https://www.iana.org/domains/root/db/sv.html +sv +com.sv +edu.sv +gob.sv +org.sv +red.sv + +// sx : https://www.iana.org/domains/root/db/sx.html +// Submitted by registry +sx +gov.sx + +// sy : https://www.iana.org/domains/root/db/sy.html +sy +com.sy +edu.sy +gov.sy +mil.sy +net.sy +org.sy + +// sz : https://www.iana.org/domains/root/db/sz.html +// http://www.sispa.org.sz/ +sz +ac.sz +co.sz +org.sz + +// tc : https://www.iana.org/domains/root/db/tc.html +tc + +// td : https://www.iana.org/domains/root/db/td.html +td + +// tel : https://www.iana.org/domains/root/db/tel.html +// http://www.telnic.org/ +tel + +// tf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +tf + +// tg : https://www.iana.org/domains/root/db/tg.html +// http://www.nic.tg/ +tg + +// th : https://www.iana.org/domains/root/db/th.html +// Submitted by registry +th +ac.th +co.th +go.th +in.th +mi.th +net.th +or.th + +// tj : http://www.nic.tj/policy.html +tj +ac.tj +biz.tj +co.tj +com.tj +edu.tj +go.tj +gov.tj +int.tj +mil.tj +name.tj +net.tj +nic.tj +org.tj +test.tj +web.tj + +// tk : https://www.iana.org/domains/root/db/tk.html +tk + +// tl : https://www.iana.org/domains/root/db/tl.html +tl +gov.tl + +// tm : https://www.nic.tm/local.html +// Confirmed by registry 2024-11-19 +tm +co.tm +com.tm +edu.tm +gov.tm +mil.tm +net.tm +nom.tm +org.tm + +// tn : http://www.registre.tn/fr/ +// https://whois.ati.tn/ +tn +com.tn +ens.tn +fin.tn +gov.tn +ind.tn +info.tn +intl.tn +mincom.tn +nat.tn +net.tn +org.tn +perso.tn +tourism.tn + +// to : https://www.iana.org/domains/root/db/to.html +// Submitted by registry +to +com.to +edu.to +gov.to +mil.to +net.to +org.to + +// tr : https://nic.tr/ +// https://nic.tr/forms/eng/policies.pdf +// https://nic.tr/index.php?USRACTN=PRICELST +tr +av.tr +bbs.tr +bel.tr +biz.tr +com.tr +dr.tr +edu.tr +gen.tr +gov.tr +info.tr +k12.tr +kep.tr +mil.tr +name.tr +net.tr +org.tr +pol.tr +tel.tr +tsk.tr +tv.tr +web.tr +// Used by Northern Cyprus +nc.tr +// Used by government agencies of Northern Cyprus +gov.nc.tr + +// tt : https://www.nic.tt/ +// Confirmed by registry 2024-11-19 +tt +biz.tt +co.tt +com.tt +edu.tt +gov.tt +info.tt +mil.tt +name.tt +net.tt +org.tt +pro.tt + +// tv : https://www.iana.org/domains/root/db/tv.html +// Not listing any 2LDs as reserved since none seem to exist in practice, +// Wikipedia notwithstanding. +tv + +// tw : https://www.iana.org/domains/root/db/tw.html +// https://twnic.tw/dnservice_catag.php +// Confirmed by registry 2024-11-26 +tw +club.tw +com.tw +ebiz.tw +edu.tw +game.tw +gov.tw +idv.tw +mil.tw +net.tw +org.tw + +// tz : http://www.tznic.or.tz/index.php/domains +// Submitted by registry +tz +ac.tz +co.tz +go.tz +hotel.tz +info.tz +me.tz +mil.tz +mobi.tz +ne.tz +or.tz +sc.tz +tv.tz + +// ua : https://hostmaster.ua/policy/?ua +// Submitted by registry +ua +// ua 2LD +com.ua +edu.ua +gov.ua +in.ua +net.ua +org.ua +// ua geographic names +// https://hostmaster.ua/2ld/ +cherkassy.ua +cherkasy.ua +chernigov.ua +chernihiv.ua +chernivtsi.ua +chernovtsy.ua +ck.ua +cn.ua +cr.ua +crimea.ua +cv.ua +dn.ua +dnepropetrovsk.ua +dnipropetrovsk.ua +donetsk.ua +dp.ua +if.ua +ivano-frankivsk.ua +kh.ua +kharkiv.ua +kharkov.ua +kherson.ua +khmelnitskiy.ua +khmelnytskyi.ua +kiev.ua +kirovograd.ua +km.ua +kr.ua +kropyvnytskyi.ua +krym.ua +ks.ua +kv.ua +kyiv.ua +lg.ua +lt.ua +lugansk.ua +luhansk.ua +lutsk.ua +lv.ua +lviv.ua +mk.ua +mykolaiv.ua +nikolaev.ua +od.ua +odesa.ua +odessa.ua +pl.ua +poltava.ua +rivne.ua +rovno.ua +rv.ua +sb.ua +sebastopol.ua +sevastopol.ua +sm.ua +sumy.ua +te.ua +ternopil.ua +uz.ua +uzhgorod.ua +uzhhorod.ua +vinnica.ua +vinnytsia.ua +vn.ua +volyn.ua +yalta.ua +zakarpattia.ua +zaporizhzhe.ua +zaporizhzhia.ua +zhitomir.ua +zhytomyr.ua +zp.ua +zt.ua + +// ug : https://www.registry.co.ug/ +// https://www.registry.co.ug, https://whois.co.ug +// Confirmed by registry 2025-01-20 +ug +ac.ug +co.ug +com.ug +edu.ug +go.ug +gov.ug +mil.ug +ne.ug +or.ug +org.ug +sc.ug +us.ug + +// uk : https://www.iana.org/domains/root/db/uk.html +// Submitted by registry +uk +ac.uk +co.uk +gov.uk +ltd.uk +me.uk +net.uk +nhs.uk +org.uk +plc.uk +police.uk +*.sch.uk + +// us : https://www.iana.org/domains/root/db/us.html +// Confirmed via the .us zone file by William Harrison 2024-12-10 +us +dni.us +isa.us +nsn.us +// Geographic Names +ak.us +al.us +ar.us +as.us +az.us +ca.us +co.us +ct.us +dc.us +de.us +fl.us +ga.us +gu.us +hi.us +ia.us +id.us +il.us +in.us +ks.us +ky.us +la.us +ma.us +md.us +me.us +mi.us +mn.us +mo.us +ms.us +mt.us +nc.us +nd.us +ne.us +nh.us +nj.us +nm.us +nv.us +ny.us +oh.us +ok.us +or.us +pa.us +pr.us +ri.us +sc.us +sd.us +tn.us +tx.us +ut.us +va.us +vi.us +vt.us +wa.us +wi.us +wv.us +wy.us +// The registrar notes several more specific domains available in each state, +// such as state.*.us, dst.*.us, etc., but resolution of these is somewhat +// haphazard; in some states these domains resolve as addresses, while in others +// only subdomains are available, or even nothing at all. We include the +// most common ones where it's clear that different sites are different +// entities. +k12.ak.us +k12.al.us +k12.ar.us +k12.as.us +k12.az.us +k12.ca.us +k12.co.us +k12.ct.us +k12.dc.us +k12.fl.us +k12.ga.us +k12.gu.us +// k12.hi.us - Bug 614565 - Hawaii has a state-wide DOE login +k12.ia.us +k12.id.us +k12.il.us +k12.in.us +k12.ks.us +k12.ky.us +k12.la.us +k12.ma.us +k12.md.us +k12.me.us +k12.mi.us +k12.mn.us +k12.mo.us +k12.ms.us +k12.mt.us +k12.nc.us +// k12.nd.us - Bug 1028347 - Removed at request of Travis Rosso +k12.ne.us +k12.nh.us +k12.nj.us +k12.nm.us +k12.nv.us +k12.ny.us +k12.oh.us +k12.ok.us +k12.or.us +k12.pa.us +k12.pr.us +// k12.ri.us - Removed at request of Kim Cournoyer +k12.sc.us +// k12.sd.us - Bug 934131 - Removed at request of James Booze +k12.tn.us +k12.tx.us +k12.ut.us +k12.va.us +k12.vi.us +k12.vt.us +k12.wa.us +k12.wi.us +// k12.wv.us - Bug 947705 - Removed at request of Verne Britton +cc.ak.us +lib.ak.us +cc.al.us +lib.al.us +cc.ar.us +lib.ar.us +cc.as.us +lib.as.us +cc.az.us +lib.az.us +cc.ca.us +lib.ca.us +cc.co.us +lib.co.us +cc.ct.us +lib.ct.us +cc.dc.us +lib.dc.us +cc.de.us +cc.fl.us +lib.fl.us +cc.ga.us +lib.ga.us +cc.gu.us +lib.gu.us +cc.hi.us +lib.hi.us +cc.ia.us +lib.ia.us +cc.id.us +lib.id.us +cc.il.us +lib.il.us +cc.in.us +lib.in.us +cc.ks.us +lib.ks.us +cc.ky.us +lib.ky.us +cc.la.us +lib.la.us +cc.ma.us +lib.ma.us +cc.md.us +lib.md.us +cc.me.us +lib.me.us +cc.mi.us +lib.mi.us +cc.mn.us +lib.mn.us +cc.mo.us +lib.mo.us +cc.ms.us +cc.mt.us +lib.mt.us +cc.nc.us +lib.nc.us +cc.nd.us +lib.nd.us +cc.ne.us +lib.ne.us +cc.nh.us +lib.nh.us +cc.nj.us +lib.nj.us +cc.nm.us +lib.nm.us +cc.nv.us +lib.nv.us +cc.ny.us +lib.ny.us +cc.oh.us +lib.oh.us +cc.ok.us +lib.ok.us +cc.or.us +lib.or.us +cc.pa.us +lib.pa.us +cc.pr.us +lib.pr.us +cc.ri.us +lib.ri.us +cc.sc.us +lib.sc.us +cc.sd.us +lib.sd.us +cc.tn.us +lib.tn.us +cc.tx.us +lib.tx.us +cc.ut.us +lib.ut.us +cc.va.us +lib.va.us +cc.vi.us +lib.vi.us +cc.vt.us +lib.vt.us +cc.wa.us +lib.wa.us +cc.wi.us +lib.wi.us +cc.wv.us +cc.wy.us +k12.wy.us +// lib.wv.us - Bug 941670 - Removed at request of Larry W Arnold +lib.wy.us +// k12.ma.us contains school districts in Massachusetts. The 4LDs are +// managed independently except for private (PVT), charter (CHTR) and +// parochial (PAROCH) schools. Those are delegated directly to the +// 5LD operators. +chtr.k12.ma.us +paroch.k12.ma.us +pvt.k12.ma.us +// Merit Network, Inc. maintains the registry for =~ /(k12|cc|lib).mi.us/ and the following +// see also: https://domreg.merit.edu : domreg@merit.edu +// see also: whois -h whois.domreg.merit.edu help +ann-arbor.mi.us +cog.mi.us +dst.mi.us +eaton.mi.us +gen.mi.us +mus.mi.us +tec.mi.us +washtenaw.mi.us + +// uy : http://www.nic.org.uy/ +uy +com.uy +edu.uy +gub.uy +mil.uy +net.uy +org.uy + +// uz : http://www.reg.uz/ +uz +co.uz +com.uz +net.uz +org.uz + +// va : https://www.iana.org/domains/root/db/va.html +va + +// vc : https://www.iana.org/domains/root/db/vc.html +// Submitted by registry +vc +com.vc +edu.vc +gov.vc +mil.vc +net.vc +org.vc + +// ve : https://registro.nic.ve/ +// https://nic.ve/site/user-agreement -> under "III. Clasificación de Nombres de Dominio" +// Submitted by registry nic@nic.ve and nicve@conatel.gob.ve +ve +arts.ve +bib.ve +co.ve +com.ve +e12.ve +edu.ve +emprende.ve +firm.ve +gob.ve +gov.ve +ia.ve +info.ve +int.ve +mil.ve +net.ve +nom.ve +org.ve +rar.ve +rec.ve +store.ve +tec.ve +web.ve + +// vg : https://www.iana.org/domains/root/db/vg.html +// Confirmed by registry 2025-01-10 +vg +edu.vg + +// vi : https://www.iana.org/domains/root/db/vi.html +vi +co.vi +com.vi +k12.vi +net.vi +org.vi + +// vn : https://www.vnnic.vn/en/domain/cctld-vn +// https://vnnic.vn/sites/default/files/tailieu/vn.cctld.domains.txt +vn +ac.vn +ai.vn +biz.vn +com.vn +edu.vn +gov.vn +health.vn +id.vn +info.vn +int.vn +io.vn +name.vn +net.vn +org.vn +pro.vn + +// vn geographical names +angiang.vn +bacgiang.vn +backan.vn +baclieu.vn +bacninh.vn +baria-vungtau.vn +bentre.vn +binhdinh.vn +binhduong.vn +binhphuoc.vn +binhthuan.vn +camau.vn +cantho.vn +caobang.vn +daklak.vn +daknong.vn +danang.vn +dienbien.vn +dongnai.vn +dongthap.vn +gialai.vn +hagiang.vn +haiduong.vn +haiphong.vn +hanam.vn +hanoi.vn +hatinh.vn +haugiang.vn +hoabinh.vn +hungyen.vn +khanhhoa.vn +kiengiang.vn +kontum.vn +laichau.vn +lamdong.vn +langson.vn +laocai.vn +longan.vn +namdinh.vn +nghean.vn +ninhbinh.vn +ninhthuan.vn +phutho.vn +phuyen.vn +quangbinh.vn +quangnam.vn +quangngai.vn +quangninh.vn +quangtri.vn +soctrang.vn +sonla.vn +tayninh.vn +thaibinh.vn +thainguyen.vn +thanhhoa.vn +thanhphohochiminh.vn +thuathienhue.vn +tiengiang.vn +travinh.vn +tuyenquang.vn +vinhlong.vn +vinhphuc.vn +yenbai.vn + +// vu : https://www.iana.org/domains/root/db/vu.html +// http://www.vunic.vu/ +vu +com.vu +edu.vu +net.vu +org.vu + +// wf : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +wf + +// ws : https://www.iana.org/domains/root/db/ws.html +// http://samoanic.ws/index.dhtml +ws +com.ws +edu.ws +gov.ws +net.ws +org.ws + +// yt : https://www.afnic.fr/wp-media/uploads/2022/12/afnic-naming-policy-2023-01-01.pdf +yt + +// IDN ccTLDs +// When submitting patches, please maintain a sort by ISO 3166 ccTLD, then +// U-label, and follow this format: +// // A-Label ("", [, variant info]) : +// // [sponsoring org] +// U-Label + +// xn--mgbaam7a8h ("Emerat", Arabic) : AE +// http://nic.ae/english/arabicdomain/rules.jsp +امارات + +// xn--y9a3aq ("hye", Armenian) : AM +// ISOC AM (operated by .am Registry) +հայ + +// xn--54b7fta0cc ("Bangla", Bangla) : BD +বাংলা + +// xn--90ae ("bg", Bulgarian) : BG +бг + +// xn--mgbcpq6gpa1a ("albahrain", Arabic) : BH +البحرين + +// xn--90ais ("bel", Belarusian/Russian Cyrillic) : BY +// Operated by .by registry +бел + +// xn--fiqs8s ("Zhongguo/China", Chinese, Simplified) : CN +// CNNIC +// https://www.cnnic.cn/11/192/index.html +中国 + +// xn--fiqz9s ("Zhongguo/China", Chinese, Traditional) : CN +// CNNIC +// https://www.cnnic.com.cn/AU/MediaC/Announcement/201609/t20160905_54470.htm +中國 + +// xn--lgbbat1ad8j ("Algeria/Al Jazair", Arabic) : DZ +الجزائر + +// xn--wgbh1c ("Egypt/Masr", Arabic) : EG +// http://www.dotmasr.eg/ +مصر + +// xn--e1a4c ("eu", Cyrillic) : EU +// https://eurid.eu +ею + +// xn--qxa6a ("eu", Greek) : EU +// https://eurid.eu +ευ + +// xn--mgbah1a3hjkrd ("Mauritania", Arabic) : MR +موريتانيا + +// xn--node ("ge", Georgian Mkhedruli) : GE +გე + +// xn--qxam ("el", Greek) : GR +// Hellenic Ministry of Infrastructure, Transport, and Networks +ελ + +// xn--j6w193g ("Hong Kong", Chinese) : HK +// https://www.hkirc.hk +// Submitted by registry +// https://www.hkirc.hk/content.jsp?id=30#!/34 +香港 +個人.香港 +公司.香港 +政府.香港 +教育.香港 +組織.香港 +網絡.香港 + +// xn--2scrj9c ("Bharat", Kannada) : IN +// India +ಭಾರತ + +// xn--3hcrj9c ("Bharat", Oriya) : IN +// India +ଭାରତ + +// xn--45br5cyl ("Bharatam", Assamese) : IN +// India +ভাৰত + +// xn--h2breg3eve ("Bharatam", Sanskrit) : IN +// India +भारतम् + +// xn--h2brj9c8c ("Bharot", Santali) : IN +// India +भारोत + +// xn--mgbgu82a ("Bharat", Sindhi) : IN +// India +ڀارت + +// xn--rvc1e0am3e ("Bharatam", Malayalam) : IN +// India +ഭാരതം + +// xn--h2brj9c ("Bharat", Devanagari) : IN +// India +भारत + +// xn--mgbbh1a ("Bharat", Kashmiri) : IN +// India +بارت + +// xn--mgbbh1a71e ("Bharat", Arabic) : IN +// India +بھارت + +// xn--fpcrj9c3d ("Bharat", Telugu) : IN +// India +భారత్ + +// xn--gecrj9c ("Bharat", Gujarati) : IN +// India +ભારત + +// xn--s9brj9c ("Bharat", Gurmukhi) : IN +// India +ਭਾਰਤ + +// xn--45brj9c ("Bharat", Bengali) : IN +// India +ভারত + +// xn--xkc2dl3a5ee0h ("India", Tamil) : IN +// India +இந்தியா + +// xn--mgba3a4f16a ("Iran", Persian) : IR +ایران + +// xn--mgba3a4fra ("Iran", Arabic) : IR +ايران + +// xn--mgbtx2b ("Iraq", Arabic) : IQ +// Communications and Media Commission +عراق + +// xn--mgbayh7gpa ("al-Ordon", Arabic) : JO +// National Information Technology Center (NITC) +// Royal Scientific Society, Al-Jubeiha +الاردن + +// xn--3e0b707e ("Republic of Korea", Hangul) : KR +한국 + +// xn--80ao21a ("Kaz", Kazakh) : KZ +қаз + +// xn--q7ce6a ("Lao", Lao) : LA +ລາວ + +// xn--fzc2c9e2c ("Lanka", Sinhalese-Sinhala) : LK +// https://nic.lk +ලංකා + +// xn--xkc2al3hye2a ("Ilangai", Tamil) : LK +// https://nic.lk +இலங்கை + +// xn--mgbc0a9azcg ("Morocco/al-Maghrib", Arabic) : MA +المغرب + +// xn--d1alf ("mkd", Macedonian) : MK +// MARnet +мкд + +// xn--l1acc ("mon", Mongolian) : MN +мон + +// xn--mix891f ("Macao", Chinese, Traditional) : MO +// MONIC / HNET Asia (Registry Operator for .mo) +澳門 + +// xn--mix082f ("Macao", Chinese, Simplified) : MO +澳门 + +// xn--mgbx4cd0ab ("Malaysia", Malay) : MY +مليسيا + +// xn--mgb9awbf ("Oman", Arabic) : OM +عمان + +// xn--mgbai9azgqp6j ("Pakistan", Urdu/Arabic) : PK +پاکستان + +// xn--mgbai9a5eva00b ("Pakistan", Urdu/Arabic, variant) : PK +پاكستان + +// xn--ygbi2ammx ("Falasteen", Arabic) : PS +// The Palestinian National Internet Naming Authority (PNINA) +// http://www.pnina.ps +فلسطين + +// xn--90a3ac ("srb", Cyrillic) : RS +// https://www.rnids.rs/en/domains/national-domains +срб +ак.срб +обр.срб +од.срб +орг.срб +пр.срб +упр.срб + +// xn--p1ai ("rf", Russian-Cyrillic) : RU +// https://cctld.ru/files/pdf/docs/en/rules_ru-rf.pdf +// Submitted by George Georgievsky +рф + +// xn--wgbl6a ("Qatar", Arabic) : QA +// http://www.ict.gov.qa/ +قطر + +// xn--mgberp4a5d4ar ("AlSaudiah", Arabic) : SA +// http://www.nic.net.sa/ +السعودية + +// xn--mgberp4a5d4a87g ("AlSaudiah", Arabic, variant): SA +السعودیة + +// xn--mgbqly7c0a67fbc ("AlSaudiah", Arabic, variant) : SA +السعودیۃ + +// xn--mgbqly7cvafr ("AlSaudiah", Arabic, variant) : SA +السعوديه + +// xn--mgbpl2fh ("sudan", Arabic) : SD +// Operated by .sd registry +سودان + +// xn--yfro4i67o Singapore ("Singapore", Chinese) : SG +新加坡 + +// xn--clchc0ea0b2g2a9gcd ("Singapore", Tamil) : SG +சிங்கப்பூர் + +// xn--ogbpf8fl ("Syria", Arabic) : SY +سورية + +// xn--mgbtf8fl ("Syria", Arabic, variant) : SY +سوريا + +// xn--o3cw4h ("Thai", Thai) : TH +// http://www.thnic.co.th +ไทย +ทหาร.ไทย +ธุรกิจ.ไทย +เน็ต.ไทย +รัฐบาล.ไทย +ศึกษา.ไทย +องค์กร.ไทย + +// xn--pgbs0dh ("Tunisia", Arabic) : TN +// http://nic.tn +تونس + +// xn--kpry57d ("Taiwan", Chinese, Traditional) : TW +// https://twnic.tw/dnservice_catag.php +台灣 + +// xn--kprw13d ("Taiwan", Chinese, Simplified) : TW +// http://www.twnic.net/english/dn/dn_07a.htm +台湾 + +// xn--nnx388a ("Taiwan", Chinese, variant) : TW +臺灣 + +// xn--j1amh ("ukr", Cyrillic) : UA +укр + +// xn--mgb2ddes ("AlYemen", Arabic) : YE +اليمن + +// xxx : http://icmregistry.com +xxx + +// ye : http://www.y.net.ye/services/domain_name.htm +ye +com.ye +edu.ye +gov.ye +mil.ye +net.ye +org.ye + +// za : https://www.iana.org/domains/root/db/za.html +ac.za +agric.za +alt.za +co.za +edu.za +gov.za +grondar.za +law.za +mil.za +net.za +ngo.za +nic.za +nis.za +nom.za +org.za +school.za +tm.za +web.za + +// zm : https://zicta.zm/ +// Submitted by registry +zm +ac.zm +biz.zm +co.zm +com.zm +edu.zm +gov.zm +info.zm +mil.zm +net.zm +org.zm +sch.zm + +// zw : https://www.potraz.gov.zw/ +// Confirmed by registry 2017-01-25 +zw +ac.zw +co.zw +gov.zw +mil.zw +org.zw + +// newGTLDs + +// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2026-01-29T15:35:06Z +// This list is auto-generated, don't edit it manually. +// aaa : American Automobile Association, Inc. +// https://www.iana.org/domains/root/db/aaa.html +aaa + +// aarp : AARP +// https://www.iana.org/domains/root/db/aarp.html +aarp + +// abb : ABB Ltd +// https://www.iana.org/domains/root/db/abb.html +abb + +// abbott : Abbott Laboratories, Inc. +// https://www.iana.org/domains/root/db/abbott.html +abbott + +// abbvie : AbbVie Inc. +// https://www.iana.org/domains/root/db/abbvie.html +abbvie + +// abc : Disney Enterprises, Inc. +// https://www.iana.org/domains/root/db/abc.html +abc + +// able : Able Inc. +// https://www.iana.org/domains/root/db/able.html +able + +// abogado : Registry Services, LLC +// https://www.iana.org/domains/root/db/abogado.html +abogado + +// abudhabi : Abu Dhabi Systems and Information Centre +// https://www.iana.org/domains/root/db/abudhabi.html +abudhabi + +// academy : Binky Moon, LLC +// https://www.iana.org/domains/root/db/academy.html +academy + +// accenture : Accenture plc +// https://www.iana.org/domains/root/db/accenture.html +accenture + +// accountant : dot Accountant Limited +// https://www.iana.org/domains/root/db/accountant.html +accountant + +// accountants : Binky Moon, LLC +// https://www.iana.org/domains/root/db/accountants.html +accountants + +// aco : ACO Severin Ahlmann GmbH & Co. KG +// https://www.iana.org/domains/root/db/aco.html +aco + +// actor : Dog Beach, LLC +// https://www.iana.org/domains/root/db/actor.html +actor + +// ads : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/ads.html +ads + +// adult : ICM Registry AD LLC +// https://www.iana.org/domains/root/db/adult.html +adult + +// aeg : Aktiebolaget Electrolux +// https://www.iana.org/domains/root/db/aeg.html +aeg + +// aetna : Aetna Life Insurance Company +// https://www.iana.org/domains/root/db/aetna.html +aetna + +// afl : Australian Football League +// https://www.iana.org/domains/root/db/afl.html +afl + +// africa : ZA Central Registry NPC trading as Registry.Africa +// https://www.iana.org/domains/root/db/africa.html +africa + +// agakhan : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/agakhan.html +agakhan + +// agency : Binky Moon, LLC +// https://www.iana.org/domains/root/db/agency.html +agency + +// aig : American International Group, Inc. +// https://www.iana.org/domains/root/db/aig.html +aig + +// airbus : Airbus S.A.S. +// https://www.iana.org/domains/root/db/airbus.html +airbus + +// airforce : Dog Beach, LLC +// https://www.iana.org/domains/root/db/airforce.html +airforce + +// airtel : Bharti Airtel Limited +// https://www.iana.org/domains/root/db/airtel.html +airtel + +// akdn : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/akdn.html +akdn + +// alibaba : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/alibaba.html +alibaba + +// alipay : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/alipay.html +alipay + +// allfinanz : Allfinanz Deutsche Vermögensberatung Aktiengesellschaft +// https://www.iana.org/domains/root/db/allfinanz.html +allfinanz + +// allstate : Allstate Fire and Casualty Insurance Company +// https://www.iana.org/domains/root/db/allstate.html +allstate + +// ally : Ally Financial Inc. +// https://www.iana.org/domains/root/db/ally.html +ally + +// alsace : Region Grand Est +// https://www.iana.org/domains/root/db/alsace.html +alsace + +// alstom : ALSTOM +// https://www.iana.org/domains/root/db/alstom.html +alstom + +// amazon : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/amazon.html +amazon + +// americanexpress : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/americanexpress.html +americanexpress + +// americanfamily : AmFam, Inc. +// https://www.iana.org/domains/root/db/americanfamily.html +americanfamily + +// amex : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/amex.html +amex + +// amfam : AmFam, Inc. +// https://www.iana.org/domains/root/db/amfam.html +amfam + +// amica : Amica Mutual Insurance Company +// https://www.iana.org/domains/root/db/amica.html +amica + +// amsterdam : Gemeente Amsterdam +// https://www.iana.org/domains/root/db/amsterdam.html +amsterdam + +// analytics : Campus IP LLC +// https://www.iana.org/domains/root/db/analytics.html +analytics + +// android : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/android.html +android + +// anquan : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/anquan.html +anquan + +// anz : Australia and New Zealand Banking Group Limited +// https://www.iana.org/domains/root/db/anz.html +anz + +// aol : Yahoo Inc. +// https://www.iana.org/domains/root/db/aol.html +aol + +// apartments : Binky Moon, LLC +// https://www.iana.org/domains/root/db/apartments.html +apartments + +// app : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/app.html +app + +// apple : Apple Inc. +// https://www.iana.org/domains/root/db/apple.html +apple + +// aquarelle : Aquarelle.com +// https://www.iana.org/domains/root/db/aquarelle.html +aquarelle + +// arab : League of Arab States +// https://www.iana.org/domains/root/db/arab.html +arab + +// aramco : Aramco Services Company +// https://www.iana.org/domains/root/db/aramco.html +aramco + +// archi : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/archi.html +archi + +// army : Dog Beach, LLC +// https://www.iana.org/domains/root/db/army.html +army + +// art : UK Creative Ideas Limited +// https://www.iana.org/domains/root/db/art.html +art + +// arte : Association Relative à la Télévision Européenne G.E.I.E. +// https://www.iana.org/domains/root/db/arte.html +arte + +// asda : Asda Stores Limited +// https://www.iana.org/domains/root/db/asda.html +asda + +// associates : Binky Moon, LLC +// https://www.iana.org/domains/root/db/associates.html +associates + +// athleta : The Gap, Inc. +// https://www.iana.org/domains/root/db/athleta.html +athleta + +// attorney : Dog Beach, LLC +// https://www.iana.org/domains/root/db/attorney.html +attorney + +// auction : Dog Beach, LLC +// https://www.iana.org/domains/root/db/auction.html +auction + +// audi : AUDI Aktiengesellschaft +// https://www.iana.org/domains/root/db/audi.html +audi + +// audible : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/audible.html +audible + +// audio : XYZ.COM LLC +// https://www.iana.org/domains/root/db/audio.html +audio + +// auspost : Australian Postal Corporation +// https://www.iana.org/domains/root/db/auspost.html +auspost + +// author : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/author.html +author + +// auto : XYZ.COM LLC +// https://www.iana.org/domains/root/db/auto.html +auto + +// autos : XYZ.COM LLC +// https://www.iana.org/domains/root/db/autos.html +autos + +// aws : AWS Registry LLC +// https://www.iana.org/domains/root/db/aws.html +aws + +// axa : AXA Group Operations SAS +// https://www.iana.org/domains/root/db/axa.html +axa + +// azure : Microsoft Corporation +// https://www.iana.org/domains/root/db/azure.html +azure + +// baby : XYZ.COM LLC +// https://www.iana.org/domains/root/db/baby.html +baby + +// baidu : Baidu, Inc. +// https://www.iana.org/domains/root/db/baidu.html +baidu + +// banamex : Citigroup Inc. +// https://www.iana.org/domains/root/db/banamex.html +banamex + +// band : Dog Beach, LLC +// https://www.iana.org/domains/root/db/band.html +band + +// bank : fTLD Registry Services LLC +// https://www.iana.org/domains/root/db/bank.html +bank + +// bar : Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +// https://www.iana.org/domains/root/db/bar.html +bar + +// barcelona : Municipi de Barcelona +// https://www.iana.org/domains/root/db/barcelona.html +barcelona + +// barclaycard : Barclays Bank PLC +// https://www.iana.org/domains/root/db/barclaycard.html +barclaycard + +// barclays : Barclays Bank PLC +// https://www.iana.org/domains/root/db/barclays.html +barclays + +// barefoot : Gallo Vineyards, Inc. +// https://www.iana.org/domains/root/db/barefoot.html +barefoot + +// bargains : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bargains.html +bargains + +// baseball : MLB Advanced Media DH, LLC +// https://www.iana.org/domains/root/db/baseball.html +baseball + +// basketball : Fédération Internationale de Basketball (FIBA) +// https://www.iana.org/domains/root/db/basketball.html +basketball + +// bauhaus : Werkhaus GmbH +// https://www.iana.org/domains/root/db/bauhaus.html +bauhaus + +// bayern : Bayern Connect GmbH +// https://www.iana.org/domains/root/db/bayern.html +bayern + +// bbc : British Broadcasting Corporation +// https://www.iana.org/domains/root/db/bbc.html +bbc + +// bbt : BB&T Corporation +// https://www.iana.org/domains/root/db/bbt.html +bbt + +// bbva : BANCO BILBAO VIZCAYA ARGENTARIA, S.A. +// https://www.iana.org/domains/root/db/bbva.html +bbva + +// bcg : The Boston Consulting Group, Inc. +// https://www.iana.org/domains/root/db/bcg.html +bcg + +// bcn : Municipi de Barcelona +// https://www.iana.org/domains/root/db/bcn.html +bcn + +// beats : Beats Electronics, LLC +// https://www.iana.org/domains/root/db/beats.html +beats + +// beauty : XYZ.COM LLC +// https://www.iana.org/domains/root/db/beauty.html +beauty + +// beer : Registry Services, LLC +// https://www.iana.org/domains/root/db/beer.html +beer + +// berlin : dotBERLIN GmbH & Co. KG +// https://www.iana.org/domains/root/db/berlin.html +berlin + +// best : BestTLD Pty Ltd +// https://www.iana.org/domains/root/db/best.html +best + +// bestbuy : BBY Solutions, Inc. +// https://www.iana.org/domains/root/db/bestbuy.html +bestbuy + +// bet : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/bet.html +bet + +// bharti : Bharti Enterprises (Holding) Private Limited +// https://www.iana.org/domains/root/db/bharti.html +bharti + +// bible : American Bible Society +// https://www.iana.org/domains/root/db/bible.html +bible + +// bid : dot Bid Limited +// https://www.iana.org/domains/root/db/bid.html +bid + +// bike : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bike.html +bike + +// bing : Microsoft Corporation +// https://www.iana.org/domains/root/db/bing.html +bing + +// bingo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/bingo.html +bingo + +// bio : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/bio.html +bio + +// black : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/black.html +black + +// blackfriday : Registry Services, LLC +// https://www.iana.org/domains/root/db/blackfriday.html +blackfriday + +// blockbuster : Dish DBS Corporation +// https://www.iana.org/domains/root/db/blockbuster.html +blockbuster + +// blog : Knock Knock WHOIS There, LLC +// https://www.iana.org/domains/root/db/blog.html +blog + +// bloomberg : Bloomberg IP Holdings LLC +// https://www.iana.org/domains/root/db/bloomberg.html +bloomberg + +// blue : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/blue.html +blue + +// bms : Bristol-Myers Squibb Company +// https://www.iana.org/domains/root/db/bms.html +bms + +// bmw : Bayerische Motoren Werke Aktiengesellschaft +// https://www.iana.org/domains/root/db/bmw.html +bmw + +// bnpparibas : BNP Paribas +// https://www.iana.org/domains/root/db/bnpparibas.html +bnpparibas + +// boats : XYZ.COM LLC +// https://www.iana.org/domains/root/db/boats.html +boats + +// boehringer : Boehringer Ingelheim International GmbH +// https://www.iana.org/domains/root/db/boehringer.html +boehringer + +// bofa : Bank of America Corporation +// https://www.iana.org/domains/root/db/bofa.html +bofa + +// bom : Núcleo de Informação e Coordenação do Ponto BR - NIC.br +// https://www.iana.org/domains/root/db/bom.html +bom + +// bond : ShortDot SA +// https://www.iana.org/domains/root/db/bond.html +bond + +// boo : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/boo.html +boo + +// book : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/book.html +book + +// booking : Booking.com B.V. +// https://www.iana.org/domains/root/db/booking.html +booking + +// bosch : Robert Bosch GMBH +// https://www.iana.org/domains/root/db/bosch.html +bosch + +// bostik : Bostik SA +// https://www.iana.org/domains/root/db/bostik.html +bostik + +// boston : Registry Services, LLC +// https://www.iana.org/domains/root/db/boston.html +boston + +// bot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/bot.html +bot + +// boutique : Binky Moon, LLC +// https://www.iana.org/domains/root/db/boutique.html +boutique + +// box : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/box.html +box + +// bradesco : Banco Bradesco S.A. +// https://www.iana.org/domains/root/db/bradesco.html +bradesco + +// bridgestone : Bridgestone Corporation +// https://www.iana.org/domains/root/db/bridgestone.html +bridgestone + +// broadway : Celebrate Broadway, Inc. +// https://www.iana.org/domains/root/db/broadway.html +broadway + +// broker : Dog Beach, LLC +// https://www.iana.org/domains/root/db/broker.html +broker + +// brother : Brother Industries, Ltd. +// https://www.iana.org/domains/root/db/brother.html +brother + +// brussels : DNS.be vzw +// https://www.iana.org/domains/root/db/brussels.html +brussels + +// build : Plan Bee LLC +// https://www.iana.org/domains/root/db/build.html +build + +// builders : Binky Moon, LLC +// https://www.iana.org/domains/root/db/builders.html +builders + +// business : Binky Moon, LLC +// https://www.iana.org/domains/root/db/business.html +business + +// buy : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/buy.html +buy + +// buzz : DOTSTRATEGY CO. +// https://www.iana.org/domains/root/db/buzz.html +buzz + +// bzh : Association www.bzh +// https://www.iana.org/domains/root/db/bzh.html +bzh + +// cab : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cab.html +cab + +// cafe : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cafe.html +cafe + +// cal : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/cal.html +cal + +// call : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/call.html +call + +// calvinklein : PVH gTLD Holdings LLC +// https://www.iana.org/domains/root/db/calvinklein.html +calvinklein + +// cam : Cam Connecting SARL +// https://www.iana.org/domains/root/db/cam.html +cam + +// camera : Binky Moon, LLC +// https://www.iana.org/domains/root/db/camera.html +camera + +// camp : Binky Moon, LLC +// https://www.iana.org/domains/root/db/camp.html +camp + +// canon : Canon Inc. +// https://www.iana.org/domains/root/db/canon.html +canon + +// capetown : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/capetown.html +capetown + +// capital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/capital.html +capital + +// capitalone : Capital One Financial Corporation +// https://www.iana.org/domains/root/db/capitalone.html +capitalone + +// car : XYZ.COM LLC +// https://www.iana.org/domains/root/db/car.html +car + +// caravan : Caravan International, Inc. +// https://www.iana.org/domains/root/db/caravan.html +caravan + +// cards : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cards.html +cards + +// care : Binky Moon, LLC +// https://www.iana.org/domains/root/db/care.html +care + +// career : dotCareer LLC +// https://www.iana.org/domains/root/db/career.html +career + +// careers : Binky Moon, LLC +// https://www.iana.org/domains/root/db/careers.html +careers + +// cars : XYZ.COM LLC +// https://www.iana.org/domains/root/db/cars.html +cars + +// casa : Registry Services, LLC +// https://www.iana.org/domains/root/db/casa.html +casa + +// case : Digity, LLC +// https://www.iana.org/domains/root/db/case.html +case + +// cash : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cash.html +cash + +// casino : Binky Moon, LLC +// https://www.iana.org/domains/root/db/casino.html +casino + +// catering : Binky Moon, LLC +// https://www.iana.org/domains/root/db/catering.html +catering + +// catholic : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/catholic.html +catholic + +// cba : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/cba.html +cba + +// cbn : The Christian Broadcasting Network, Inc. +// https://www.iana.org/domains/root/db/cbn.html +cbn + +// cbre : CBRE, Inc. +// https://www.iana.org/domains/root/db/cbre.html +cbre + +// center : Binky Moon, LLC +// https://www.iana.org/domains/root/db/center.html +center + +// ceo : XYZ.COM LLC +// https://www.iana.org/domains/root/db/ceo.html +ceo + +// cern : European Organization for Nuclear Research ("CERN") +// https://www.iana.org/domains/root/db/cern.html +cern + +// cfa : CFA Institute +// https://www.iana.org/domains/root/db/cfa.html +cfa + +// cfd : ShortDot SA +// https://www.iana.org/domains/root/db/cfd.html +cfd + +// chanel : Chanel International B.V. +// https://www.iana.org/domains/root/db/chanel.html +chanel + +// channel : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/channel.html +channel + +// charity : Public Interest Registry +// https://www.iana.org/domains/root/db/charity.html +charity + +// chase : JPMorgan Chase Bank, National Association +// https://www.iana.org/domains/root/db/chase.html +chase + +// chat : Binky Moon, LLC +// https://www.iana.org/domains/root/db/chat.html +chat + +// cheap : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cheap.html +cheap + +// chintai : CHINTAI Corporation +// https://www.iana.org/domains/root/db/chintai.html +chintai + +// christmas : XYZ.COM LLC +// https://www.iana.org/domains/root/db/christmas.html +christmas + +// chrome : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/chrome.html +chrome + +// church : Binky Moon, LLC +// https://www.iana.org/domains/root/db/church.html +church + +// cipriani : Hotel Cipriani Srl +// https://www.iana.org/domains/root/db/cipriani.html +cipriani + +// circle : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/circle.html +circle + +// cisco : Cisco Technology, Inc. +// https://www.iana.org/domains/root/db/cisco.html +cisco + +// citadel : Citadel Domain LLC +// https://www.iana.org/domains/root/db/citadel.html +citadel + +// citi : Citigroup Inc. +// https://www.iana.org/domains/root/db/citi.html +citi + +// citic : CITIC Group Corporation +// https://www.iana.org/domains/root/db/citic.html +citic + +// city : Binky Moon, LLC +// https://www.iana.org/domains/root/db/city.html +city + +// claims : Binky Moon, LLC +// https://www.iana.org/domains/root/db/claims.html +claims + +// cleaning : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cleaning.html +cleaning + +// click : Waterford Limited +// https://www.iana.org/domains/root/db/click.html +click + +// clinic : Binky Moon, LLC +// https://www.iana.org/domains/root/db/clinic.html +clinic + +// clinique : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/clinique.html +clinique + +// clothing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/clothing.html +clothing + +// cloud : Aruba PEC S.p.A. +// https://www.iana.org/domains/root/db/cloud.html +cloud + +// club : Registry Services, LLC +// https://www.iana.org/domains/root/db/club.html +club + +// clubmed : Club Méditerranée S.A. +// https://www.iana.org/domains/root/db/clubmed.html +clubmed + +// coach : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coach.html +coach + +// codes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/codes.html +codes + +// coffee : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coffee.html +coffee + +// college : XYZ.COM LLC +// https://www.iana.org/domains/root/db/college.html +college + +// cologne : dotKoeln GmbH +// https://www.iana.org/domains/root/db/cologne.html +cologne + +// commbank : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/commbank.html +commbank + +// community : Binky Moon, LLC +// https://www.iana.org/domains/root/db/community.html +community + +// company : Binky Moon, LLC +// https://www.iana.org/domains/root/db/company.html +company + +// compare : Registry Services, LLC +// https://www.iana.org/domains/root/db/compare.html +compare + +// computer : Binky Moon, LLC +// https://www.iana.org/domains/root/db/computer.html +computer + +// comsec : VeriSign, Inc. +// https://www.iana.org/domains/root/db/comsec.html +comsec + +// condos : Binky Moon, LLC +// https://www.iana.org/domains/root/db/condos.html +condos + +// construction : Binky Moon, LLC +// https://www.iana.org/domains/root/db/construction.html +construction + +// consulting : Dog Beach, LLC +// https://www.iana.org/domains/root/db/consulting.html +consulting + +// contact : Dog Beach, LLC +// https://www.iana.org/domains/root/db/contact.html +contact + +// contractors : Binky Moon, LLC +// https://www.iana.org/domains/root/db/contractors.html +contractors + +// cooking : Registry Services, LLC +// https://www.iana.org/domains/root/db/cooking.html +cooking + +// cool : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cool.html +cool + +// corsica : Collectivité de Corse +// https://www.iana.org/domains/root/db/corsica.html +corsica + +// country : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/country.html +country + +// coupon : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/coupon.html +coupon + +// coupons : Binky Moon, LLC +// https://www.iana.org/domains/root/db/coupons.html +coupons + +// courses : Registry Services, LLC +// https://www.iana.org/domains/root/db/courses.html +courses + +// cpa : American Institute of Certified Public Accountants +// https://www.iana.org/domains/root/db/cpa.html +cpa + +// credit : Binky Moon, LLC +// https://www.iana.org/domains/root/db/credit.html +credit + +// creditcard : Binky Moon, LLC +// https://www.iana.org/domains/root/db/creditcard.html +creditcard + +// creditunion : DotCooperation LLC +// https://www.iana.org/domains/root/db/creditunion.html +creditunion + +// cricket : dot Cricket Limited +// https://www.iana.org/domains/root/db/cricket.html +cricket + +// crown : Crown Equipment Corporation +// https://www.iana.org/domains/root/db/crown.html +crown + +// crs : Federated Co-operatives Limited +// https://www.iana.org/domains/root/db/crs.html +crs + +// cruise : Viking River Cruises (Bermuda) Ltd. +// https://www.iana.org/domains/root/db/cruise.html +cruise + +// cruises : Binky Moon, LLC +// https://www.iana.org/domains/root/db/cruises.html +cruises + +// cuisinella : SCHMIDT GROUPE S.A.S. +// https://www.iana.org/domains/root/db/cuisinella.html +cuisinella + +// cymru : Nominet UK +// https://www.iana.org/domains/root/db/cymru.html +cymru + +// cyou : ShortDot SA +// https://www.iana.org/domains/root/db/cyou.html +cyou + +// dad : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dad.html +dad + +// dance : Dog Beach, LLC +// https://www.iana.org/domains/root/db/dance.html +dance + +// data : Dish DBS Corporation +// https://www.iana.org/domains/root/db/data.html +data + +// date : dot Date Limited +// https://www.iana.org/domains/root/db/date.html +date + +// dating : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dating.html +dating + +// datsun : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/datsun.html +datsun + +// day : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/day.html +day + +// dclk : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dclk.html +dclk + +// dds : Registry Services, LLC +// https://www.iana.org/domains/root/db/dds.html +dds + +// deal : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/deal.html +deal + +// dealer : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/dealer.html +dealer + +// deals : Binky Moon, LLC +// https://www.iana.org/domains/root/db/deals.html +deals + +// degree : Dog Beach, LLC +// https://www.iana.org/domains/root/db/degree.html +degree + +// delivery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/delivery.html +delivery + +// dell : Dell Inc. +// https://www.iana.org/domains/root/db/dell.html +dell + +// deloitte : Deloitte Touche Tohmatsu +// https://www.iana.org/domains/root/db/deloitte.html +deloitte + +// delta : Delta Air Lines, Inc. +// https://www.iana.org/domains/root/db/delta.html +delta + +// democrat : Dog Beach, LLC +// https://www.iana.org/domains/root/db/democrat.html +democrat + +// dental : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dental.html +dental + +// dentist : Dog Beach, LLC +// https://www.iana.org/domains/root/db/dentist.html +dentist + +// desi +// https://www.iana.org/domains/root/db/desi.html +desi + +// design : Registry Services, LLC +// https://www.iana.org/domains/root/db/design.html +design + +// dev : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/dev.html +dev + +// dhl : Deutsche Post AG +// https://www.iana.org/domains/root/db/dhl.html +dhl + +// diamonds : Binky Moon, LLC +// https://www.iana.org/domains/root/db/diamonds.html +diamonds + +// diet : XYZ.COM LLC +// https://www.iana.org/domains/root/db/diet.html +diet + +// digital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/digital.html +digital + +// direct : Binky Moon, LLC +// https://www.iana.org/domains/root/db/direct.html +direct + +// directory : Binky Moon, LLC +// https://www.iana.org/domains/root/db/directory.html +directory + +// discount : Binky Moon, LLC +// https://www.iana.org/domains/root/db/discount.html +discount + +// discover : Discover Financial Services +// https://www.iana.org/domains/root/db/discover.html +discover + +// dish : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dish.html +dish + +// diy : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/diy.html +diy + +// dnp : Dai Nippon Printing Co., Ltd. +// https://www.iana.org/domains/root/db/dnp.html +dnp + +// docs : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/docs.html +docs + +// doctor : Binky Moon, LLC +// https://www.iana.org/domains/root/db/doctor.html +doctor + +// dog : Binky Moon, LLC +// https://www.iana.org/domains/root/db/dog.html +dog + +// domains : Binky Moon, LLC +// https://www.iana.org/domains/root/db/domains.html +domains + +// dot : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dot.html +dot + +// download : dot Support Limited +// https://www.iana.org/domains/root/db/download.html +download + +// drive : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/drive.html +drive + +// dtv : Dish DBS Corporation +// https://www.iana.org/domains/root/db/dtv.html +dtv + +// dubai : Dubai Smart Government Department +// https://www.iana.org/domains/root/db/dubai.html +dubai + +// dupont : DuPont Specialty Products USA, LLC +// https://www.iana.org/domains/root/db/dupont.html +dupont + +// durban : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/durban.html +durban + +// dvag : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/dvag.html +dvag + +// dvr : DISH Technologies L.L.C. +// https://www.iana.org/domains/root/db/dvr.html +dvr + +// earth : Interlink Systems Innovation Institute K.K. +// https://www.iana.org/domains/root/db/earth.html +earth + +// eat : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/eat.html +eat + +// eco : Big Room Inc. +// https://www.iana.org/domains/root/db/eco.html +eco + +// edeka : EDEKA Verband kaufmännischer Genossenschaften e.V. +// https://www.iana.org/domains/root/db/edeka.html +edeka + +// education : Binky Moon, LLC +// https://www.iana.org/domains/root/db/education.html +education + +// email : Binky Moon, LLC +// https://www.iana.org/domains/root/db/email.html +email + +// emerck : Merck KGaA +// https://www.iana.org/domains/root/db/emerck.html +emerck + +// energy : Binky Moon, LLC +// https://www.iana.org/domains/root/db/energy.html +energy + +// engineer : Dog Beach, LLC +// https://www.iana.org/domains/root/db/engineer.html +engineer + +// engineering : Binky Moon, LLC +// https://www.iana.org/domains/root/db/engineering.html +engineering + +// enterprises : Binky Moon, LLC +// https://www.iana.org/domains/root/db/enterprises.html +enterprises + +// epson : Seiko Epson Corporation +// https://www.iana.org/domains/root/db/epson.html +epson + +// equipment : Binky Moon, LLC +// https://www.iana.org/domains/root/db/equipment.html +equipment + +// ericsson : Telefonaktiebolaget L M Ericsson +// https://www.iana.org/domains/root/db/ericsson.html +ericsson + +// erni : ERNI Group Holding AG +// https://www.iana.org/domains/root/db/erni.html +erni + +// esq : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/esq.html +esq + +// estate : Binky Moon, LLC +// https://www.iana.org/domains/root/db/estate.html +estate + +// eurovision : European Broadcasting Union (EBU) +// https://www.iana.org/domains/root/db/eurovision.html +eurovision + +// eus : Puntueus Fundazioa +// https://www.iana.org/domains/root/db/eus.html +eus + +// events : Binky Moon, LLC +// https://www.iana.org/domains/root/db/events.html +events + +// exchange : Binky Moon, LLC +// https://www.iana.org/domains/root/db/exchange.html +exchange + +// expert : Binky Moon, LLC +// https://www.iana.org/domains/root/db/expert.html +expert + +// exposed : Binky Moon, LLC +// https://www.iana.org/domains/root/db/exposed.html +exposed + +// express : Binky Moon, LLC +// https://www.iana.org/domains/root/db/express.html +express + +// extraspace : Extra Space Storage LLC +// https://www.iana.org/domains/root/db/extraspace.html +extraspace + +// fage : Fage International S.A. +// https://www.iana.org/domains/root/db/fage.html +fage + +// fail : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fail.html +fail + +// fairwinds : FairWinds Partners, LLC +// https://www.iana.org/domains/root/db/fairwinds.html +fairwinds + +// faith : dot Faith Limited +// https://www.iana.org/domains/root/db/faith.html +faith + +// family : Dog Beach, LLC +// https://www.iana.org/domains/root/db/family.html +family + +// fan : Dog Beach, LLC +// https://www.iana.org/domains/root/db/fan.html +fan + +// fans : ZDNS International Limited +// https://www.iana.org/domains/root/db/fans.html +fans + +// farm : Binky Moon, LLC +// https://www.iana.org/domains/root/db/farm.html +farm + +// farmers : Farmers Insurance Exchange +// https://www.iana.org/domains/root/db/farmers.html +farmers + +// fashion : Registry Services, LLC +// https://www.iana.org/domains/root/db/fashion.html +fashion + +// fast : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/fast.html +fast + +// fedex : Federal Express Corporation +// https://www.iana.org/domains/root/db/fedex.html +fedex + +// feedback : Top Level Spectrum, Inc. +// https://www.iana.org/domains/root/db/feedback.html +feedback + +// ferrari : Fiat Chrysler Automobiles N.V. +// https://www.iana.org/domains/root/db/ferrari.html +ferrari + +// ferrero : Ferrero Trading Lux S.A. +// https://www.iana.org/domains/root/db/ferrero.html +ferrero + +// fidelity : Fidelity Brokerage Services LLC +// https://www.iana.org/domains/root/db/fidelity.html +fidelity + +// fido : Rogers Communications Canada Inc. +// https://www.iana.org/domains/root/db/fido.html +fido + +// film : Motion Picture Domain Registry Pty Ltd +// https://www.iana.org/domains/root/db/film.html +film + +// final : Núcleo de Informação e Coordenação do Ponto BR - NIC.br +// https://www.iana.org/domains/root/db/final.html +final + +// finance : Binky Moon, LLC +// https://www.iana.org/domains/root/db/finance.html +finance + +// financial : Binky Moon, LLC +// https://www.iana.org/domains/root/db/financial.html +financial + +// fire : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/fire.html +fire + +// firestone : Bridgestone Licensing Services, Inc +// https://www.iana.org/domains/root/db/firestone.html +firestone + +// firmdale : Firmdale Holdings Limited +// https://www.iana.org/domains/root/db/firmdale.html +firmdale + +// fish : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fish.html +fish + +// fishing : Registry Services, LLC +// https://www.iana.org/domains/root/db/fishing.html +fishing + +// fit : Registry Services, LLC +// https://www.iana.org/domains/root/db/fit.html +fit + +// fitness : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fitness.html +fitness + +// flickr : Flickr, Inc. +// https://www.iana.org/domains/root/db/flickr.html +flickr + +// flights : Binky Moon, LLC +// https://www.iana.org/domains/root/db/flights.html +flights + +// flir : FLIR Systems, Inc. +// https://www.iana.org/domains/root/db/flir.html +flir + +// florist : Binky Moon, LLC +// https://www.iana.org/domains/root/db/florist.html +florist + +// flowers : XYZ.COM LLC +// https://www.iana.org/domains/root/db/flowers.html +flowers + +// fly : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/fly.html +fly + +// foo : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/foo.html +foo + +// food : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/food.html +food + +// football : Binky Moon, LLC +// https://www.iana.org/domains/root/db/football.html +football + +// ford : Ford Motor Company +// https://www.iana.org/domains/root/db/ford.html +ford + +// forex : Dog Beach, LLC +// https://www.iana.org/domains/root/db/forex.html +forex + +// forsale : Dog Beach, LLC +// https://www.iana.org/domains/root/db/forsale.html +forsale + +// forum : Waterford Limited +// https://www.iana.org/domains/root/db/forum.html +forum + +// foundation : Public Interest Registry +// https://www.iana.org/domains/root/db/foundation.html +foundation + +// fox : FOX Registry, LLC +// https://www.iana.org/domains/root/db/fox.html +fox + +// free : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/free.html +free + +// fresenius : Fresenius Immobilien-Verwaltungs-GmbH +// https://www.iana.org/domains/root/db/fresenius.html +fresenius + +// frl : FRLregistry B.V. +// https://www.iana.org/domains/root/db/frl.html +frl + +// frogans : OP3FT +// https://www.iana.org/domains/root/db/frogans.html +frogans + +// frontier : Frontier Communications Corporation +// https://www.iana.org/domains/root/db/frontier.html +frontier + +// ftr : Frontier Communications Corporation +// https://www.iana.org/domains/root/db/ftr.html +ftr + +// fujitsu : Fujitsu Limited +// https://www.iana.org/domains/root/db/fujitsu.html +fujitsu + +// fun : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/fun.html +fun + +// fund : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fund.html +fund + +// furniture : Binky Moon, LLC +// https://www.iana.org/domains/root/db/furniture.html +furniture + +// futbol : Dog Beach, LLC +// https://www.iana.org/domains/root/db/futbol.html +futbol + +// fyi : Binky Moon, LLC +// https://www.iana.org/domains/root/db/fyi.html +fyi + +// gal : Asociación puntoGAL +// https://www.iana.org/domains/root/db/gal.html +gal + +// gallery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gallery.html +gallery + +// gallo : Gallo Vineyards, Inc. +// https://www.iana.org/domains/root/db/gallo.html +gallo + +// gallup : Gallup, Inc. +// https://www.iana.org/domains/root/db/gallup.html +gallup + +// game : XYZ.COM LLC +// https://www.iana.org/domains/root/db/game.html +game + +// games : Dog Beach, LLC +// https://www.iana.org/domains/root/db/games.html +games + +// gap : The Gap, Inc. +// https://www.iana.org/domains/root/db/gap.html +gap + +// garden : Registry Services, LLC +// https://www.iana.org/domains/root/db/garden.html +garden + +// gay : Registry Services, LLC +// https://www.iana.org/domains/root/db/gay.html +gay + +// gbiz : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gbiz.html +gbiz + +// gdn : Joint Stock Company "Navigation-information systems" +// https://www.iana.org/domains/root/db/gdn.html +gdn + +// gea : GEA Group Aktiengesellschaft +// https://www.iana.org/domains/root/db/gea.html +gea + +// gent : Easyhost BV +// https://www.iana.org/domains/root/db/gent.html +gent + +// genting : Resorts World Inc Pte. Ltd. +// https://www.iana.org/domains/root/db/genting.html +genting + +// george : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/george.html +george + +// ggee : GMO Internet, Inc. +// https://www.iana.org/domains/root/db/ggee.html +ggee + +// gift : DotGift, LLC +// https://www.iana.org/domains/root/db/gift.html +gift + +// gifts : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gifts.html +gifts + +// gives : Public Interest Registry +// https://www.iana.org/domains/root/db/gives.html +gives + +// giving : Public Interest Registry +// https://www.iana.org/domains/root/db/giving.html +giving + +// glass : Binky Moon, LLC +// https://www.iana.org/domains/root/db/glass.html +glass + +// gle : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gle.html +gle + +// global : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/global.html +global + +// globo : Globo Comunicação e Participações S.A +// https://www.iana.org/domains/root/db/globo.html +globo + +// gmail : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/gmail.html +gmail + +// gmbh : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gmbh.html +gmbh + +// gmo : GMO Internet, Inc. +// https://www.iana.org/domains/root/db/gmo.html +gmo + +// gmx : 1&1 Mail & Media GmbH +// https://www.iana.org/domains/root/db/gmx.html +gmx + +// godaddy : Go Daddy East, LLC +// https://www.iana.org/domains/root/db/godaddy.html +godaddy + +// gold : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gold.html +gold + +// goldpoint : YODOBASHI CAMERA CO.,LTD. +// https://www.iana.org/domains/root/db/goldpoint.html +goldpoint + +// golf : Binky Moon, LLC +// https://www.iana.org/domains/root/db/golf.html +golf + +// goo : NTT DOCOMO, INC. +// https://www.iana.org/domains/root/db/goo.html +goo + +// goodyear : The Goodyear Tire & Rubber Company +// https://www.iana.org/domains/root/db/goodyear.html +goodyear + +// goog : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/goog.html +goog + +// google : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/google.html +google + +// gop : Republican State Leadership Committee, Inc. +// https://www.iana.org/domains/root/db/gop.html +gop + +// got : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/got.html +got + +// grainger : Grainger Registry Services, LLC +// https://www.iana.org/domains/root/db/grainger.html +grainger + +// graphics : Binky Moon, LLC +// https://www.iana.org/domains/root/db/graphics.html +graphics + +// gratis : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gratis.html +gratis + +// green : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/green.html +green + +// gripe : Binky Moon, LLC +// https://www.iana.org/domains/root/db/gripe.html +gripe + +// grocery : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/grocery.html +grocery + +// group : Binky Moon, LLC +// https://www.iana.org/domains/root/db/group.html +group + +// gucci : Guccio Gucci S.p.a. +// https://www.iana.org/domains/root/db/gucci.html +gucci + +// guge : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/guge.html +guge + +// guide : Binky Moon, LLC +// https://www.iana.org/domains/root/db/guide.html +guide + +// guitars : XYZ.COM LLC +// https://www.iana.org/domains/root/db/guitars.html +guitars + +// guru : Binky Moon, LLC +// https://www.iana.org/domains/root/db/guru.html +guru + +// hair : XYZ.COM LLC +// https://www.iana.org/domains/root/db/hair.html +hair + +// hamburg : Hamburg Top-Level-Domain GmbH +// https://www.iana.org/domains/root/db/hamburg.html +hamburg + +// hangout : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/hangout.html +hangout + +// haus : Dog Beach, LLC +// https://www.iana.org/domains/root/db/haus.html +haus + +// hbo : HBO Registry Services, Inc. +// https://www.iana.org/domains/root/db/hbo.html +hbo + +// hdfc : HDFC BANK LIMITED +// https://www.iana.org/domains/root/db/hdfc.html +hdfc + +// hdfcbank : HDFC BANK LIMITED +// https://www.iana.org/domains/root/db/hdfcbank.html +hdfcbank + +// health : Registry Services, LLC +// https://www.iana.org/domains/root/db/health.html +health + +// healthcare : Binky Moon, LLC +// https://www.iana.org/domains/root/db/healthcare.html +healthcare + +// help : Innovation service Limited +// https://www.iana.org/domains/root/db/help.html +help + +// helsinki : City of Helsinki +// https://www.iana.org/domains/root/db/helsinki.html +helsinki + +// here : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/here.html +here + +// hermes : HERMES INTERNATIONAL +// https://www.iana.org/domains/root/db/hermes.html +hermes + +// hiphop : Dot Hip Hop, LLC +// https://www.iana.org/domains/root/db/hiphop.html +hiphop + +// hisamitsu : Hisamitsu Pharmaceutical Co.,Inc. +// https://www.iana.org/domains/root/db/hisamitsu.html +hisamitsu + +// hitachi : Hitachi, Ltd. +// https://www.iana.org/domains/root/db/hitachi.html +hitachi + +// hiv : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/hiv.html +hiv + +// hkt : PCCW-HKT DataCom Services Limited +// https://www.iana.org/domains/root/db/hkt.html +hkt + +// hockey : Binky Moon, LLC +// https://www.iana.org/domains/root/db/hockey.html +hockey + +// holdings : Binky Moon, LLC +// https://www.iana.org/domains/root/db/holdings.html +holdings + +// holiday : Binky Moon, LLC +// https://www.iana.org/domains/root/db/holiday.html +holiday + +// homedepot : Home Depot Product Authority, LLC +// https://www.iana.org/domains/root/db/homedepot.html +homedepot + +// homegoods : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/homegoods.html +homegoods + +// homes : XYZ.COM LLC +// https://www.iana.org/domains/root/db/homes.html +homes + +// homesense : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/homesense.html +homesense + +// honda : Honda Motor Co., Ltd. +// https://www.iana.org/domains/root/db/honda.html +honda + +// horse : Registry Services, LLC +// https://www.iana.org/domains/root/db/horse.html +horse + +// hospital : Binky Moon, LLC +// https://www.iana.org/domains/root/db/hospital.html +hospital + +// host : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/host.html +host + +// hosting : XYZ.COM LLC +// https://www.iana.org/domains/root/db/hosting.html +hosting + +// hot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/hot.html +hot + +// hotel : HOTEL Top-Level-Domain S.a.r.l +// https://www.iana.org/domains/root/db/hotel.html +hotel + +// hotels : Booking.com B.V. +// https://www.iana.org/domains/root/db/hotels.html +hotels + +// hotmail : Microsoft Corporation +// https://www.iana.org/domains/root/db/hotmail.html +hotmail + +// house : Binky Moon, LLC +// https://www.iana.org/domains/root/db/house.html +house + +// how : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/how.html +how + +// hsbc : HSBC Global Services (UK) Limited +// https://www.iana.org/domains/root/db/hsbc.html +hsbc + +// hughes : Hughes Satellite Systems Corporation +// https://www.iana.org/domains/root/db/hughes.html +hughes + +// hyatt : Hyatt GTLD, L.L.C. +// https://www.iana.org/domains/root/db/hyatt.html +hyatt + +// hyundai : Hyundai Motor Company +// https://www.iana.org/domains/root/db/hyundai.html +hyundai + +// ibm : International Business Machines Corporation +// https://www.iana.org/domains/root/db/ibm.html +ibm + +// icbc : Industrial and Commercial Bank of China Limited +// https://www.iana.org/domains/root/db/icbc.html +icbc + +// ice : IntercontinentalExchange, Inc. +// https://www.iana.org/domains/root/db/ice.html +ice + +// icu : ShortDot SA +// https://www.iana.org/domains/root/db/icu.html +icu + +// ieee : IEEE Global LLC +// https://www.iana.org/domains/root/db/ieee.html +ieee + +// ifm : ifm electronic gmbh +// https://www.iana.org/domains/root/db/ifm.html +ifm + +// ikano : Ikano S.A. +// https://www.iana.org/domains/root/db/ikano.html +ikano + +// imamat : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/imamat.html +imamat + +// imdb : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/imdb.html +imdb + +// immo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/immo.html +immo + +// immobilien : Dog Beach, LLC +// https://www.iana.org/domains/root/db/immobilien.html +immobilien + +// inc : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/inc.html +inc + +// industries : Binky Moon, LLC +// https://www.iana.org/domains/root/db/industries.html +industries + +// infiniti : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/infiniti.html +infiniti + +// ing : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/ing.html +ing + +// ink : Registry Services, LLC +// https://www.iana.org/domains/root/db/ink.html +ink + +// institute : Binky Moon, LLC +// https://www.iana.org/domains/root/db/institute.html +institute + +// insurance : fTLD Registry Services LLC +// https://www.iana.org/domains/root/db/insurance.html +insurance + +// insure : Binky Moon, LLC +// https://www.iana.org/domains/root/db/insure.html +insure + +// international : Binky Moon, LLC +// https://www.iana.org/domains/root/db/international.html +international + +// intuit : Intuit Administrative Services, Inc. +// https://www.iana.org/domains/root/db/intuit.html +intuit + +// investments : Binky Moon, LLC +// https://www.iana.org/domains/root/db/investments.html +investments + +// ipiranga : Ipiranga Produtos de Petroleo S.A. +// https://www.iana.org/domains/root/db/ipiranga.html +ipiranga + +// irish : Binky Moon, LLC +// https://www.iana.org/domains/root/db/irish.html +irish + +// ismaili : Fondation Aga Khan (Aga Khan Foundation) +// https://www.iana.org/domains/root/db/ismaili.html +ismaili + +// ist : Istanbul Metropolitan Municipality +// https://www.iana.org/domains/root/db/ist.html +ist + +// istanbul : Istanbul Metropolitan Municipality +// https://www.iana.org/domains/root/db/istanbul.html +istanbul + +// itau : Itau Unibanco Holding S.A. +// https://www.iana.org/domains/root/db/itau.html +itau + +// itv : ITV Services Limited +// https://www.iana.org/domains/root/db/itv.html +itv + +// jaguar : Jaguar Land Rover Ltd +// https://www.iana.org/domains/root/db/jaguar.html +jaguar + +// java : Oracle Corporation +// https://www.iana.org/domains/root/db/java.html +java + +// jcb : JCB Co., Ltd. +// https://www.iana.org/domains/root/db/jcb.html +jcb + +// jeep : FCA US LLC. +// https://www.iana.org/domains/root/db/jeep.html +jeep + +// jetzt : Binky Moon, LLC +// https://www.iana.org/domains/root/db/jetzt.html +jetzt + +// jewelry : Binky Moon, LLC +// https://www.iana.org/domains/root/db/jewelry.html +jewelry + +// jio : Reliance Industries Limited +// https://www.iana.org/domains/root/db/jio.html +jio + +// jll : Jones Lang LaSalle Incorporated +// https://www.iana.org/domains/root/db/jll.html +jll + +// jmp : Matrix IP LLC +// https://www.iana.org/domains/root/db/jmp.html +jmp + +// jnj : Johnson & Johnson Services, Inc. +// https://www.iana.org/domains/root/db/jnj.html +jnj + +// joburg : ZA Central Registry NPC trading as ZA Central Registry +// https://www.iana.org/domains/root/db/joburg.html +joburg + +// jot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/jot.html +jot + +// joy : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/joy.html +joy + +// jpmorgan : JPMorgan Chase Bank, National Association +// https://www.iana.org/domains/root/db/jpmorgan.html +jpmorgan + +// jprs : Japan Registry Services Co., Ltd. +// https://www.iana.org/domains/root/db/jprs.html +jprs + +// juegos : Dog Beach, LLC +// https://www.iana.org/domains/root/db/juegos.html +juegos + +// juniper : JUNIPER NETWORKS, INC. +// https://www.iana.org/domains/root/db/juniper.html +juniper + +// kaufen : Dog Beach, LLC +// https://www.iana.org/domains/root/db/kaufen.html +kaufen + +// kddi : KDDI CORPORATION +// https://www.iana.org/domains/root/db/kddi.html +kddi + +// kerryhotels : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kerryhotels.html +kerryhotels + +// kerryproperties : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kerryproperties.html +kerryproperties + +// kfh : Kuwait Finance House +// https://www.iana.org/domains/root/db/kfh.html +kfh + +// kia : KIA MOTORS CORPORATION +// https://www.iana.org/domains/root/db/kia.html +kia + +// kids : DotKids Foundation Limited +// https://www.iana.org/domains/root/db/kids.html +kids + +// kim : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/kim.html +kim + +// kindle : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/kindle.html +kindle + +// kitchen : Binky Moon, LLC +// https://www.iana.org/domains/root/db/kitchen.html +kitchen + +// kiwi : DOT KIWI LIMITED +// https://www.iana.org/domains/root/db/kiwi.html +kiwi + +// koeln : dotKoeln GmbH +// https://www.iana.org/domains/root/db/koeln.html +koeln + +// komatsu : Komatsu Ltd. +// https://www.iana.org/domains/root/db/komatsu.html +komatsu + +// kosher : Kosher Marketing Assets LLC +// https://www.iana.org/domains/root/db/kosher.html +kosher + +// kpmg : KPMG International Cooperative (KPMG International Genossenschaft) +// https://www.iana.org/domains/root/db/kpmg.html +kpmg + +// kpn : Koninklijke KPN N.V. +// https://www.iana.org/domains/root/db/kpn.html +kpn + +// krd : KRG Department of Information Technology +// https://www.iana.org/domains/root/db/krd.html +krd + +// kred : KredTLD Pty Ltd +// https://www.iana.org/domains/root/db/kred.html +kred + +// kuokgroup : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/kuokgroup.html +kuokgroup + +// kyoto : Academic Institution: Kyoto Jyoho Gakuen +// https://www.iana.org/domains/root/db/kyoto.html +kyoto + +// lacaixa : Fundación Bancaria Caixa d’Estalvis i Pensions de Barcelona, “la Caixa” +// https://www.iana.org/domains/root/db/lacaixa.html +lacaixa + +// lamborghini : Automobili Lamborghini S.p.A. +// https://www.iana.org/domains/root/db/lamborghini.html +lamborghini + +// lamer : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/lamer.html +lamer + +// land : Binky Moon, LLC +// https://www.iana.org/domains/root/db/land.html +land + +// landrover : Jaguar Land Rover Ltd +// https://www.iana.org/domains/root/db/landrover.html +landrover + +// lanxess : LANXESS Corporation +// https://www.iana.org/domains/root/db/lanxess.html +lanxess + +// lasalle : Jones Lang LaSalle Incorporated +// https://www.iana.org/domains/root/db/lasalle.html +lasalle + +// lat : XYZ.COM LLC +// https://www.iana.org/domains/root/db/lat.html +lat + +// latino : Dish DBS Corporation +// https://www.iana.org/domains/root/db/latino.html +latino + +// latrobe : La Trobe University +// https://www.iana.org/domains/root/db/latrobe.html +latrobe + +// law : Registry Services, LLC +// https://www.iana.org/domains/root/db/law.html +law + +// lawyer : Dog Beach, LLC +// https://www.iana.org/domains/root/db/lawyer.html +lawyer + +// lds : IRI Domain Management, LLC +// https://www.iana.org/domains/root/db/lds.html +lds + +// lease : Binky Moon, LLC +// https://www.iana.org/domains/root/db/lease.html +lease + +// leclerc : A.C.D. LEC Association des Centres Distributeurs Edouard Leclerc +// https://www.iana.org/domains/root/db/leclerc.html +leclerc + +// lefrak : LeFrak Organization, Inc. +// https://www.iana.org/domains/root/db/lefrak.html +lefrak + +// legal : Binky Moon, LLC +// https://www.iana.org/domains/root/db/legal.html +legal + +// lego : LEGO Juris A/S +// https://www.iana.org/domains/root/db/lego.html +lego + +// lexus : TOYOTA MOTOR CORPORATION +// https://www.iana.org/domains/root/db/lexus.html +lexus + +// lgbt : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/lgbt.html +lgbt + +// lidl : Schwarz Domains und Services GmbH & Co. KG +// https://www.iana.org/domains/root/db/lidl.html +lidl + +// life : Binky Moon, LLC +// https://www.iana.org/domains/root/db/life.html +life + +// lifeinsurance : American Council of Life Insurers +// https://www.iana.org/domains/root/db/lifeinsurance.html +lifeinsurance + +// lifestyle : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/lifestyle.html +lifestyle + +// lighting : Binky Moon, LLC +// https://www.iana.org/domains/root/db/lighting.html +lighting + +// like : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/like.html +like + +// lilly : Eli Lilly and Company +// https://www.iana.org/domains/root/db/lilly.html +lilly + +// limited : Binky Moon, LLC +// https://www.iana.org/domains/root/db/limited.html +limited + +// limo : Binky Moon, LLC +// https://www.iana.org/domains/root/db/limo.html +limo + +// lincoln : Ford Motor Company +// https://www.iana.org/domains/root/db/lincoln.html +lincoln + +// link : Nova Registry Ltd +// https://www.iana.org/domains/root/db/link.html +link + +// live : Dog Beach, LLC +// https://www.iana.org/domains/root/db/live.html +live + +// living : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/living.html +living + +// llc : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/llc.html +llc + +// llp : Intercap Registry Inc. +// https://www.iana.org/domains/root/db/llp.html +llp + +// loan : dot Loan Limited +// https://www.iana.org/domains/root/db/loan.html +loan + +// loans : Binky Moon, LLC +// https://www.iana.org/domains/root/db/loans.html +loans + +// locker : Orange Domains LLC +// https://www.iana.org/domains/root/db/locker.html +locker + +// locus : Locus Analytics LLC +// https://www.iana.org/domains/root/db/locus.html +locus + +// lol : XYZ.COM LLC +// https://www.iana.org/domains/root/db/lol.html +lol + +// london : Dot London Domains Limited +// https://www.iana.org/domains/root/db/london.html +london + +// lotte : Lotte Holdings Co., Ltd. +// https://www.iana.org/domains/root/db/lotte.html +lotte + +// lotto : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/lotto.html +lotto + +// love : Waterford Limited +// https://www.iana.org/domains/root/db/love.html +love + +// lpl : LPL Holdings, Inc. +// https://www.iana.org/domains/root/db/lpl.html +lpl + +// lplfinancial : LPL Holdings, Inc. +// https://www.iana.org/domains/root/db/lplfinancial.html +lplfinancial + +// ltd : Binky Moon, LLC +// https://www.iana.org/domains/root/db/ltd.html +ltd + +// ltda : InterNetX, Corp +// https://www.iana.org/domains/root/db/ltda.html +ltda + +// lundbeck : H. Lundbeck A/S +// https://www.iana.org/domains/root/db/lundbeck.html +lundbeck + +// luxe : Registry Services, LLC +// https://www.iana.org/domains/root/db/luxe.html +luxe + +// luxury : Luxury Partners, LLC +// https://www.iana.org/domains/root/db/luxury.html +luxury + +// madrid : Comunidad de Madrid +// https://www.iana.org/domains/root/db/madrid.html +madrid + +// maif : Mutuelle Assurance Instituteur France (MAIF) +// https://www.iana.org/domains/root/db/maif.html +maif + +// maison : Binky Moon, LLC +// https://www.iana.org/domains/root/db/maison.html +maison + +// makeup : XYZ.COM LLC +// https://www.iana.org/domains/root/db/makeup.html +makeup + +// man : MAN Truck & Bus SE +// https://www.iana.org/domains/root/db/man.html +man + +// management : Binky Moon, LLC +// https://www.iana.org/domains/root/db/management.html +management + +// mango : PUNTO FA S.L. +// https://www.iana.org/domains/root/db/mango.html +mango + +// map : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/map.html +map + +// market : Dog Beach, LLC +// https://www.iana.org/domains/root/db/market.html +market + +// marketing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/marketing.html +marketing + +// markets : Dog Beach, LLC +// https://www.iana.org/domains/root/db/markets.html +markets + +// marriott : Marriott Worldwide Corporation +// https://www.iana.org/domains/root/db/marriott.html +marriott + +// marshalls : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/marshalls.html +marshalls + +// mattel : Mattel IT Services, Inc. +// https://www.iana.org/domains/root/db/mattel.html +mattel + +// mba : Binky Moon, LLC +// https://www.iana.org/domains/root/db/mba.html +mba + +// mckinsey : McKinsey Holdings, Inc. +// https://www.iana.org/domains/root/db/mckinsey.html +mckinsey + +// med : Medistry LLC +// https://www.iana.org/domains/root/db/med.html +med + +// media : Binky Moon, LLC +// https://www.iana.org/domains/root/db/media.html +media + +// meet : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/meet.html +meet + +// melbourne : The Crown in right of the State of Victoria, represented by its Department of State Development, Business and Innovation +// https://www.iana.org/domains/root/db/melbourne.html +melbourne + +// meme : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/meme.html +meme + +// memorial : Dog Beach, LLC +// https://www.iana.org/domains/root/db/memorial.html +memorial + +// men : Exclusive Registry Limited +// https://www.iana.org/domains/root/db/men.html +men + +// menu : Dot Menu Registry, LLC +// https://www.iana.org/domains/root/db/menu.html +menu + +// merck : Merck Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/merck.html +merck + +// merckmsd : MSD Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/merckmsd.html +merckmsd + +// miami : Registry Services, LLC +// https://www.iana.org/domains/root/db/miami.html +miami + +// microsoft : Microsoft Corporation +// https://www.iana.org/domains/root/db/microsoft.html +microsoft + +// mini : Bayerische Motoren Werke Aktiengesellschaft +// https://www.iana.org/domains/root/db/mini.html +mini + +// mint : Intuit Administrative Services, Inc. +// https://www.iana.org/domains/root/db/mint.html +mint + +// mit : Massachusetts Institute of Technology +// https://www.iana.org/domains/root/db/mit.html +mit + +// mitsubishi : Mitsubishi Corporation +// https://www.iana.org/domains/root/db/mitsubishi.html +mitsubishi + +// mlb : MLB Advanced Media DH, LLC +// https://www.iana.org/domains/root/db/mlb.html +mlb + +// mls : The Canadian Real Estate Association +// https://www.iana.org/domains/root/db/mls.html +mls + +// mma : MMA IARD +// https://www.iana.org/domains/root/db/mma.html +mma + +// mobile : Dish DBS Corporation +// https://www.iana.org/domains/root/db/mobile.html +mobile + +// moda : Dog Beach, LLC +// https://www.iana.org/domains/root/db/moda.html +moda + +// moe : Interlink Systems Innovation Institute K.K. +// https://www.iana.org/domains/root/db/moe.html +moe + +// moi : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/moi.html +moi + +// mom : XYZ.COM LLC +// https://www.iana.org/domains/root/db/mom.html +mom + +// monash : Monash University +// https://www.iana.org/domains/root/db/monash.html +monash + +// money : Binky Moon, LLC +// https://www.iana.org/domains/root/db/money.html +money + +// monster : XYZ.COM LLC +// https://www.iana.org/domains/root/db/monster.html +monster + +// mormon : IRI Domain Management, LLC +// https://www.iana.org/domains/root/db/mormon.html +mormon + +// mortgage : Dog Beach, LLC +// https://www.iana.org/domains/root/db/mortgage.html +mortgage + +// moscow : Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +// https://www.iana.org/domains/root/db/moscow.html +moscow + +// moto : Motorola Trademark Holdings, LLC +// https://www.iana.org/domains/root/db/moto.html +moto + +// motorcycles : XYZ.COM LLC +// https://www.iana.org/domains/root/db/motorcycles.html +motorcycles + +// mov : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/mov.html +mov + +// movie : Binky Moon, LLC +// https://www.iana.org/domains/root/db/movie.html +movie + +// msd : MSD Registry Holdings, Inc. +// https://www.iana.org/domains/root/db/msd.html +msd + +// mtn : MTN Dubai Limited +// https://www.iana.org/domains/root/db/mtn.html +mtn + +// mtr : MTR Corporation Limited +// https://www.iana.org/domains/root/db/mtr.html +mtr + +// music : DotMusic Limited +// https://www.iana.org/domains/root/db/music.html +music + +// nab : National Australia Bank Limited +// https://www.iana.org/domains/root/db/nab.html +nab + +// nagoya : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/nagoya.html +nagoya + +// navy : Dog Beach, LLC +// https://www.iana.org/domains/root/db/navy.html +navy + +// nba : NBA REGISTRY, LLC +// https://www.iana.org/domains/root/db/nba.html +nba + +// nec : NEC Corporation +// https://www.iana.org/domains/root/db/nec.html +nec + +// netbank : COMMONWEALTH BANK OF AUSTRALIA +// https://www.iana.org/domains/root/db/netbank.html +netbank + +// netflix : Netflix, Inc. +// https://www.iana.org/domains/root/db/netflix.html +netflix + +// network : Binky Moon, LLC +// https://www.iana.org/domains/root/db/network.html +network + +// neustar : NeuStar, Inc. +// https://www.iana.org/domains/root/db/neustar.html +neustar + +// new : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/new.html +new + +// news : Dog Beach, LLC +// https://www.iana.org/domains/root/db/news.html +news + +// next : Next plc +// https://www.iana.org/domains/root/db/next.html +next + +// nextdirect : Next plc +// https://www.iana.org/domains/root/db/nextdirect.html +nextdirect + +// nexus : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/nexus.html +nexus + +// nfl : NFL Reg Ops LLC +// https://www.iana.org/domains/root/db/nfl.html +nfl + +// ngo : Public Interest Registry +// https://www.iana.org/domains/root/db/ngo.html +ngo + +// nhk : Japan Broadcasting Corporation (NHK) +// https://www.iana.org/domains/root/db/nhk.html +nhk + +// nico : DWANGO Co., Ltd. +// https://www.iana.org/domains/root/db/nico.html +nico + +// nike : NIKE, Inc. +// https://www.iana.org/domains/root/db/nike.html +nike + +// nikon : NIKON CORPORATION +// https://www.iana.org/domains/root/db/nikon.html +nikon + +// ninja : Dog Beach, LLC +// https://www.iana.org/domains/root/db/ninja.html +ninja + +// nissan : NISSAN MOTOR CO., LTD. +// https://www.iana.org/domains/root/db/nissan.html +nissan + +// nissay : Nippon Life Insurance Company +// https://www.iana.org/domains/root/db/nissay.html +nissay + +// nokia : Nokia Corporation +// https://www.iana.org/domains/root/db/nokia.html +nokia + +// norton : Gen Digital Inc. +// https://www.iana.org/domains/root/db/norton.html +norton + +// now : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/now.html +now + +// nowruz +// https://www.iana.org/domains/root/db/nowruz.html +nowruz + +// nowtv : Starbucks (HK) Limited +// https://www.iana.org/domains/root/db/nowtv.html +nowtv + +// nra : National Rifle Association of America +// https://www.iana.org/domains/root/db/nra.html +nra + +// nrw : Minds + Machines GmbH +// https://www.iana.org/domains/root/db/nrw.html +nrw + +// ntt : NIPPON TELEGRAPH AND TELEPHONE CORPORATION +// https://www.iana.org/domains/root/db/ntt.html +ntt + +// nyc : The City of New York by and through the New York City Department of Information Technology & Telecommunications +// https://www.iana.org/domains/root/db/nyc.html +nyc + +// obi : OBI Group Holding SE & Co. KGaA +// https://www.iana.org/domains/root/db/obi.html +obi + +// observer : Fegistry, LLC +// https://www.iana.org/domains/root/db/observer.html +observer + +// office : Microsoft Corporation +// https://www.iana.org/domains/root/db/office.html +office + +// okinawa : BRregistry, Inc. +// https://www.iana.org/domains/root/db/okinawa.html +okinawa + +// olayan : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/olayan.html +olayan + +// olayangroup : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/olayangroup.html +olayangroup + +// ollo : Dish DBS Corporation +// https://www.iana.org/domains/root/db/ollo.html +ollo + +// omega : The Swatch Group Ltd +// https://www.iana.org/domains/root/db/omega.html +omega + +// one : One.com A/S +// https://www.iana.org/domains/root/db/one.html +one + +// ong : Public Interest Registry +// https://www.iana.org/domains/root/db/ong.html +ong + +// onl : iRegistry GmbH +// https://www.iana.org/domains/root/db/onl.html +onl + +// online : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/online.html +online + +// ooo : INFIBEAM AVENUES LIMITED +// https://www.iana.org/domains/root/db/ooo.html +ooo + +// open : American Express Travel Related Services Company, Inc. +// https://www.iana.org/domains/root/db/open.html +open + +// oracle : Oracle Corporation +// https://www.iana.org/domains/root/db/oracle.html +oracle + +// orange : Orange Brand Services Limited +// https://www.iana.org/domains/root/db/orange.html +orange + +// organic : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/organic.html +organic + +// origins : The Estée Lauder Companies Inc. +// https://www.iana.org/domains/root/db/origins.html +origins + +// osaka : Osaka Registry Co., Ltd. +// https://www.iana.org/domains/root/db/osaka.html +osaka + +// otsuka : Otsuka Holdings Co., Ltd. +// https://www.iana.org/domains/root/db/otsuka.html +otsuka + +// ott : Dish DBS Corporation +// https://www.iana.org/domains/root/db/ott.html +ott + +// ovh : MédiaBC +// https://www.iana.org/domains/root/db/ovh.html +ovh + +// page : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/page.html +page + +// panasonic : Panasonic Holdings Corporation +// https://www.iana.org/domains/root/db/panasonic.html +panasonic + +// paris : City of Paris +// https://www.iana.org/domains/root/db/paris.html +paris + +// pars +// https://www.iana.org/domains/root/db/pars.html +pars + +// partners : Binky Moon, LLC +// https://www.iana.org/domains/root/db/partners.html +partners + +// parts : Binky Moon, LLC +// https://www.iana.org/domains/root/db/parts.html +parts + +// party : Blue Sky Registry Limited +// https://www.iana.org/domains/root/db/party.html +party + +// pay : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/pay.html +pay + +// pccw : PCCW Enterprises Limited +// https://www.iana.org/domains/root/db/pccw.html +pccw + +// pet : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/pet.html +pet + +// pfizer : Pfizer Inc. +// https://www.iana.org/domains/root/db/pfizer.html +pfizer + +// pharmacy : National Association of Boards of Pharmacy +// https://www.iana.org/domains/root/db/pharmacy.html +pharmacy + +// phd : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/phd.html +phd + +// philips : Koninklijke Philips N.V. +// https://www.iana.org/domains/root/db/philips.html +philips + +// phone : Dish DBS Corporation +// https://www.iana.org/domains/root/db/phone.html +phone + +// photo : Registry Services, LLC +// https://www.iana.org/domains/root/db/photo.html +photo + +// photography : Binky Moon, LLC +// https://www.iana.org/domains/root/db/photography.html +photography + +// photos : Binky Moon, LLC +// https://www.iana.org/domains/root/db/photos.html +photos + +// physio : PhysBiz Pty Ltd +// https://www.iana.org/domains/root/db/physio.html +physio + +// pics : XYZ.COM LLC +// https://www.iana.org/domains/root/db/pics.html +pics + +// pictet : Banque Pictet & Cie SA +// https://www.iana.org/domains/root/db/pictet.html +pictet + +// pictures : Binky Moon, LLC +// https://www.iana.org/domains/root/db/pictures.html +pictures + +// pid : Top Level Spectrum, Inc. +// https://www.iana.org/domains/root/db/pid.html +pid + +// pin : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/pin.html +pin + +// ping : Ping Registry Provider, Inc. +// https://www.iana.org/domains/root/db/ping.html +ping + +// pink : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/pink.html +pink + +// pioneer : Pioneer Corporation +// https://www.iana.org/domains/root/db/pioneer.html +pioneer + +// pizza : Binky Moon, LLC +// https://www.iana.org/domains/root/db/pizza.html +pizza + +// place : Binky Moon, LLC +// https://www.iana.org/domains/root/db/place.html +place + +// play : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/play.html +play + +// playstation : Sony Interactive Entertainment Inc. +// https://www.iana.org/domains/root/db/playstation.html +playstation + +// plumbing : Binky Moon, LLC +// https://www.iana.org/domains/root/db/plumbing.html +plumbing + +// plus : Binky Moon, LLC +// https://www.iana.org/domains/root/db/plus.html +plus + +// pnc : PNC Domain Co., LLC +// https://www.iana.org/domains/root/db/pnc.html +pnc + +// pohl : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/pohl.html +pohl + +// poker : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/poker.html +poker + +// politie : Politie Nederland +// https://www.iana.org/domains/root/db/politie.html +politie + +// porn : ICM Registry PN LLC +// https://www.iana.org/domains/root/db/porn.html +porn + +// praxi : Praxi S.p.A. +// https://www.iana.org/domains/root/db/praxi.html +praxi + +// press : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/press.html +press + +// prime : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/prime.html +prime + +// prod : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/prod.html +prod + +// productions : Binky Moon, LLC +// https://www.iana.org/domains/root/db/productions.html +productions + +// prof : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/prof.html +prof + +// progressive : Progressive Casualty Insurance Company +// https://www.iana.org/domains/root/db/progressive.html +progressive + +// promo : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/promo.html +promo + +// properties : Binky Moon, LLC +// https://www.iana.org/domains/root/db/properties.html +properties + +// property : Digital Property Infrastructure Limited +// https://www.iana.org/domains/root/db/property.html +property + +// protection : XYZ.COM LLC +// https://www.iana.org/domains/root/db/protection.html +protection + +// pru : Prudential Financial, Inc. +// https://www.iana.org/domains/root/db/pru.html +pru + +// prudential : Prudential Financial, Inc. +// https://www.iana.org/domains/root/db/prudential.html +prudential + +// pub : Dog Beach, LLC +// https://www.iana.org/domains/root/db/pub.html +pub + +// pwc : PricewaterhouseCoopers LLP +// https://www.iana.org/domains/root/db/pwc.html +pwc + +// qpon : dotQPON LLC +// https://www.iana.org/domains/root/db/qpon.html +qpon + +// quebec : PointQuébec Inc +// https://www.iana.org/domains/root/db/quebec.html +quebec + +// quest : XYZ.COM LLC +// https://www.iana.org/domains/root/db/quest.html +quest + +// racing : Premier Registry Limited +// https://www.iana.org/domains/root/db/racing.html +racing + +// radio : Digity, LLC +// https://www.iana.org/domains/root/db/radio.html +radio + +// read : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/read.html +read + +// realestate : dotRealEstate LLC +// https://www.iana.org/domains/root/db/realestate.html +realestate + +// realtor : Real Estate Domains LLC +// https://www.iana.org/domains/root/db/realtor.html +realtor + +// realty : Waterford Limited +// https://www.iana.org/domains/root/db/realty.html +realty + +// recipes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/recipes.html +recipes + +// red : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/red.html +red + +// redumbrella : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/redumbrella.html +redumbrella + +// rehab : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rehab.html +rehab + +// reise : Binky Moon, LLC +// https://www.iana.org/domains/root/db/reise.html +reise + +// reisen : Binky Moon, LLC +// https://www.iana.org/domains/root/db/reisen.html +reisen + +// reit : National Association of Real Estate Investment Trusts, Inc. +// https://www.iana.org/domains/root/db/reit.html +reit + +// reliance : Reliance Industries Limited +// https://www.iana.org/domains/root/db/reliance.html +reliance + +// ren : ZDNS International Limited +// https://www.iana.org/domains/root/db/ren.html +ren + +// rent : XYZ.COM LLC +// https://www.iana.org/domains/root/db/rent.html +rent + +// rentals : Binky Moon, LLC +// https://www.iana.org/domains/root/db/rentals.html +rentals + +// repair : Binky Moon, LLC +// https://www.iana.org/domains/root/db/repair.html +repair + +// report : Binky Moon, LLC +// https://www.iana.org/domains/root/db/report.html +report + +// republican : Dog Beach, LLC +// https://www.iana.org/domains/root/db/republican.html +republican + +// rest : Punto 2012 Sociedad Anonima Promotora de Inversion de Capital Variable +// https://www.iana.org/domains/root/db/rest.html +rest + +// restaurant : Binky Moon, LLC +// https://www.iana.org/domains/root/db/restaurant.html +restaurant + +// review : dot Review Limited +// https://www.iana.org/domains/root/db/review.html +review + +// reviews : Dog Beach, LLC +// https://www.iana.org/domains/root/db/reviews.html +reviews + +// rexroth : Robert Bosch GMBH +// https://www.iana.org/domains/root/db/rexroth.html +rexroth + +// rich : iRegistry GmbH +// https://www.iana.org/domains/root/db/rich.html +rich + +// richardli : Pacific Century Asset Management (HK) Limited +// https://www.iana.org/domains/root/db/richardli.html +richardli + +// ricoh : Ricoh Company, Ltd. +// https://www.iana.org/domains/root/db/ricoh.html +ricoh + +// ril : Reliance Industries Limited +// https://www.iana.org/domains/root/db/ril.html +ril + +// rio : Empresa Municipal de Informática SA - IPLANRIO +// https://www.iana.org/domains/root/db/rio.html +rio + +// rip : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rip.html +rip + +// rocks : Dog Beach, LLC +// https://www.iana.org/domains/root/db/rocks.html +rocks + +// rodeo : Registry Services, LLC +// https://www.iana.org/domains/root/db/rodeo.html +rodeo + +// rogers : Rogers Communications Canada Inc. +// https://www.iana.org/domains/root/db/rogers.html +rogers + +// room : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/room.html +room + +// rsvp : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/rsvp.html +rsvp + +// rugby : World Rugby Strategic Developments Limited +// https://www.iana.org/domains/root/db/rugby.html +rugby + +// ruhr : dotSaarland GmbH +// https://www.iana.org/domains/root/db/ruhr.html +ruhr + +// run : Binky Moon, LLC +// https://www.iana.org/domains/root/db/run.html +run + +// rwe : RWE AG +// https://www.iana.org/domains/root/db/rwe.html +rwe + +// ryukyu : BRregistry, Inc. +// https://www.iana.org/domains/root/db/ryukyu.html +ryukyu + +// saarland : dotSaarland GmbH +// https://www.iana.org/domains/root/db/saarland.html +saarland + +// safe : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/safe.html +safe + +// safety : Safety Registry Services, LLC. +// https://www.iana.org/domains/root/db/safety.html +safety + +// sakura : SAKURA Internet Inc. +// https://www.iana.org/domains/root/db/sakura.html +sakura + +// sale : Dog Beach, LLC +// https://www.iana.org/domains/root/db/sale.html +sale + +// salon : Binky Moon, LLC +// https://www.iana.org/domains/root/db/salon.html +salon + +// samsclub : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/samsclub.html +samsclub + +// samsung : SAMSUNG SDS CO., LTD +// https://www.iana.org/domains/root/db/samsung.html +samsung + +// sandvik : Sandvik AB +// https://www.iana.org/domains/root/db/sandvik.html +sandvik + +// sandvikcoromant : Sandvik AB +// https://www.iana.org/domains/root/db/sandvikcoromant.html +sandvikcoromant + +// sanofi : Sanofi +// https://www.iana.org/domains/root/db/sanofi.html +sanofi + +// sap : SAP AG +// https://www.iana.org/domains/root/db/sap.html +sap + +// sarl : Binky Moon, LLC +// https://www.iana.org/domains/root/db/sarl.html +sarl + +// sas : Research IP LLC +// https://www.iana.org/domains/root/db/sas.html +sas + +// save : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/save.html +save + +// saxo : Saxo Bank A/S +// https://www.iana.org/domains/root/db/saxo.html +saxo + +// sbi : STATE BANK OF INDIA +// https://www.iana.org/domains/root/db/sbi.html +sbi + +// sbs : ShortDot SA +// https://www.iana.org/domains/root/db/sbs.html +sbs + +// scb : The Siam Commercial Bank Public Company Limited ("SCB") +// https://www.iana.org/domains/root/db/scb.html +scb + +// schaeffler : Schaeffler Technologies AG & Co. KG +// https://www.iana.org/domains/root/db/schaeffler.html +schaeffler + +// schmidt : SCHMIDT GROUPE S.A.S. +// https://www.iana.org/domains/root/db/schmidt.html +schmidt + +// scholarships : Scholarships.com, LLC +// https://www.iana.org/domains/root/db/scholarships.html +scholarships + +// school : Binky Moon, LLC +// https://www.iana.org/domains/root/db/school.html +school + +// schule : Binky Moon, LLC +// https://www.iana.org/domains/root/db/schule.html +schule + +// schwarz : Schwarz Domains und Services GmbH & Co. KG +// https://www.iana.org/domains/root/db/schwarz.html +schwarz + +// science : dot Science Limited +// https://www.iana.org/domains/root/db/science.html +science + +// scot : Dot Scot Registry Limited +// https://www.iana.org/domains/root/db/scot.html +scot + +// search : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/search.html +search + +// seat : SEAT, S.A. (Sociedad Unipersonal) +// https://www.iana.org/domains/root/db/seat.html +seat + +// secure : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/secure.html +secure + +// security : XYZ.COM LLC +// https://www.iana.org/domains/root/db/security.html +security + +// seek : Seek Limited +// https://www.iana.org/domains/root/db/seek.html +seek + +// select : Registry Services, LLC +// https://www.iana.org/domains/root/db/select.html +select + +// sener : Sener Ingeniería y Sistemas, S.A. +// https://www.iana.org/domains/root/db/sener.html +sener + +// services : Binky Moon, LLC +// https://www.iana.org/domains/root/db/services.html +services + +// seven : Seven West Media Ltd +// https://www.iana.org/domains/root/db/seven.html +seven + +// sew : SEW-EURODRIVE GmbH & Co KG +// https://www.iana.org/domains/root/db/sew.html +sew + +// sex : ICM Registry SX LLC +// https://www.iana.org/domains/root/db/sex.html +sex + +// sexy : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/sexy.html +sexy + +// sfr : Societe Francaise du Radiotelephone - SFR +// https://www.iana.org/domains/root/db/sfr.html +sfr + +// shangrila : Shangri‐La International Hotel Management Limited +// https://www.iana.org/domains/root/db/shangrila.html +shangrila + +// sharp : Sharp Corporation +// https://www.iana.org/domains/root/db/sharp.html +sharp + +// shell : Shell Information Technology International Inc +// https://www.iana.org/domains/root/db/shell.html +shell + +// shia +// https://www.iana.org/domains/root/db/shia.html +shia + +// shiksha : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/shiksha.html +shiksha + +// shoes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/shoes.html +shoes + +// shop : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/shop.html +shop + +// shopping : Binky Moon, LLC +// https://www.iana.org/domains/root/db/shopping.html +shopping + +// shouji : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/shouji.html +shouji + +// show : Binky Moon, LLC +// https://www.iana.org/domains/root/db/show.html +show + +// silk : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/silk.html +silk + +// sina : Sina Corporation +// https://www.iana.org/domains/root/db/sina.html +sina + +// singles : Binky Moon, LLC +// https://www.iana.org/domains/root/db/singles.html +singles + +// site : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/site.html +site + +// ski : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/ski.html +ski + +// skin : XYZ.COM LLC +// https://www.iana.org/domains/root/db/skin.html +skin + +// sky : Sky UK Limited +// https://www.iana.org/domains/root/db/sky.html +sky + +// skype : Microsoft Corporation +// https://www.iana.org/domains/root/db/skype.html +skype + +// sling : DISH Technologies L.L.C. +// https://www.iana.org/domains/root/db/sling.html +sling + +// smart : Smart Communications, Inc. (SMART) +// https://www.iana.org/domains/root/db/smart.html +smart + +// smile : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/smile.html +smile + +// sncf : Société Nationale SNCF +// https://www.iana.org/domains/root/db/sncf.html +sncf + +// soccer : Binky Moon, LLC +// https://www.iana.org/domains/root/db/soccer.html +soccer + +// social : Dog Beach, LLC +// https://www.iana.org/domains/root/db/social.html +social + +// softbank : SoftBank Group Corp. +// https://www.iana.org/domains/root/db/softbank.html +softbank + +// software : Dog Beach, LLC +// https://www.iana.org/domains/root/db/software.html +software + +// sohu : Sohu.com Limited +// https://www.iana.org/domains/root/db/sohu.html +sohu + +// solar : Binky Moon, LLC +// https://www.iana.org/domains/root/db/solar.html +solar + +// solutions : Binky Moon, LLC +// https://www.iana.org/domains/root/db/solutions.html +solutions + +// song : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/song.html +song + +// sony : Sony Group Corporation +// https://www.iana.org/domains/root/db/sony.html +sony + +// soy : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/soy.html +soy + +// spa : Asia Spa and Wellness Promotion Council Limited +// https://www.iana.org/domains/root/db/spa.html +spa + +// space : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/space.html +space + +// sport : SportAccord +// https://www.iana.org/domains/root/db/sport.html +sport + +// spot : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/spot.html +spot + +// srl : InterNetX, Corp +// https://www.iana.org/domains/root/db/srl.html +srl + +// stada : STADA Arzneimittel AG +// https://www.iana.org/domains/root/db/stada.html +stada + +// staples : Staples, Inc. +// https://www.iana.org/domains/root/db/staples.html +staples + +// star : Star India Private Limited +// https://www.iana.org/domains/root/db/star.html +star + +// statebank : STATE BANK OF INDIA +// https://www.iana.org/domains/root/db/statebank.html +statebank + +// statefarm : State Farm Mutual Automobile Insurance Company +// https://www.iana.org/domains/root/db/statefarm.html +statefarm + +// stc : Saudi Telecom Company +// https://www.iana.org/domains/root/db/stc.html +stc + +// stcgroup : Saudi Telecom Company +// https://www.iana.org/domains/root/db/stcgroup.html +stcgroup + +// stockholm : Stockholms kommun +// https://www.iana.org/domains/root/db/stockholm.html +stockholm + +// storage : XYZ.COM LLC +// https://www.iana.org/domains/root/db/storage.html +storage + +// store : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/store.html +store + +// stream : dot Stream Limited +// https://www.iana.org/domains/root/db/stream.html +stream + +// studio : Dog Beach, LLC +// https://www.iana.org/domains/root/db/studio.html +studio + +// study : Registry Services, LLC +// https://www.iana.org/domains/root/db/study.html +study + +// style : Binky Moon, LLC +// https://www.iana.org/domains/root/db/style.html +style + +// sucks : Vox Populi Registry Ltd. +// https://www.iana.org/domains/root/db/sucks.html +sucks + +// supplies : Binky Moon, LLC +// https://www.iana.org/domains/root/db/supplies.html +supplies + +// supply : Binky Moon, LLC +// https://www.iana.org/domains/root/db/supply.html +supply + +// support : Binky Moon, LLC +// https://www.iana.org/domains/root/db/support.html +support + +// surf : Registry Services, LLC +// https://www.iana.org/domains/root/db/surf.html +surf + +// surgery : Binky Moon, LLC +// https://www.iana.org/domains/root/db/surgery.html +surgery + +// suzuki : SUZUKI MOTOR CORPORATION +// https://www.iana.org/domains/root/db/suzuki.html +suzuki + +// swatch : The Swatch Group Ltd +// https://www.iana.org/domains/root/db/swatch.html +swatch + +// swiss : Swiss Confederation +// https://www.iana.org/domains/root/db/swiss.html +swiss + +// sydney : State of New South Wales, Department of Premier and Cabinet +// https://www.iana.org/domains/root/db/sydney.html +sydney + +// systems : Binky Moon, LLC +// https://www.iana.org/domains/root/db/systems.html +systems + +// tab : Tabcorp Holdings Limited +// https://www.iana.org/domains/root/db/tab.html +tab + +// taipei : Taipei City Government +// https://www.iana.org/domains/root/db/taipei.html +taipei + +// talk : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/talk.html +talk + +// taobao : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/taobao.html +taobao + +// target : Target Domain Holdings, LLC +// https://www.iana.org/domains/root/db/target.html +target + +// tatamotors : Tata Motors Ltd +// https://www.iana.org/domains/root/db/tatamotors.html +tatamotors + +// tatar : Limited Liability Company "Coordination Center of Regional Domain of Tatarstan Republic" +// https://www.iana.org/domains/root/db/tatar.html +tatar + +// tattoo : Registry Services, LLC +// https://www.iana.org/domains/root/db/tattoo.html +tattoo + +// tax : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tax.html +tax + +// taxi : Binky Moon, LLC +// https://www.iana.org/domains/root/db/taxi.html +taxi + +// tci +// https://www.iana.org/domains/root/db/tci.html +tci + +// tdk : TDK Corporation +// https://www.iana.org/domains/root/db/tdk.html +tdk + +// team : Binky Moon, LLC +// https://www.iana.org/domains/root/db/team.html +team + +// tech : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/tech.html +tech + +// technology : Binky Moon, LLC +// https://www.iana.org/domains/root/db/technology.html +technology + +// temasek : Temasek Holdings (Private) Limited +// https://www.iana.org/domains/root/db/temasek.html +temasek + +// tennis : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tennis.html +tennis + +// teva : Teva Pharmaceutical Industries Limited +// https://www.iana.org/domains/root/db/teva.html +teva + +// thd : Home Depot Product Authority, LLC +// https://www.iana.org/domains/root/db/thd.html +thd + +// theater : Binky Moon, LLC +// https://www.iana.org/domains/root/db/theater.html +theater + +// theatre : XYZ.COM LLC +// https://www.iana.org/domains/root/db/theatre.html +theatre + +// tiaa : Teachers Insurance and Annuity Association of America +// https://www.iana.org/domains/root/db/tiaa.html +tiaa + +// tickets : XYZ.COM LLC +// https://www.iana.org/domains/root/db/tickets.html +tickets + +// tienda : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tienda.html +tienda + +// tips : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tips.html +tips + +// tires : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tires.html +tires + +// tirol : punkt Tirol GmbH +// https://www.iana.org/domains/root/db/tirol.html +tirol + +// tjmaxx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tjmaxx.html +tjmaxx + +// tjx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tjx.html +tjx + +// tkmaxx : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/tkmaxx.html +tkmaxx + +// tmall : Alibaba Group Holding Limited +// https://www.iana.org/domains/root/db/tmall.html +tmall + +// today : Binky Moon, LLC +// https://www.iana.org/domains/root/db/today.html +today + +// tokyo : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/tokyo.html +tokyo + +// tools : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tools.html +tools + +// top : Hong Kong Zhongze International Limited +// https://www.iana.org/domains/root/db/top.html +top + +// toray : Toray Industries, Inc. +// https://www.iana.org/domains/root/db/toray.html +toray + +// toshiba : TOSHIBA Corporation +// https://www.iana.org/domains/root/db/toshiba.html +toshiba + +// total : TotalEnergies SE +// https://www.iana.org/domains/root/db/total.html +total + +// tours : Binky Moon, LLC +// https://www.iana.org/domains/root/db/tours.html +tours + +// town : Binky Moon, LLC +// https://www.iana.org/domains/root/db/town.html +town + +// toyota : TOYOTA MOTOR CORPORATION +// https://www.iana.org/domains/root/db/toyota.html +toyota + +// toys : Binky Moon, LLC +// https://www.iana.org/domains/root/db/toys.html +toys + +// trade : Elite Registry Limited +// https://www.iana.org/domains/root/db/trade.html +trade + +// trading : Dog Beach, LLC +// https://www.iana.org/domains/root/db/trading.html +trading + +// training : Binky Moon, LLC +// https://www.iana.org/domains/root/db/training.html +training + +// travel : Dog Beach, LLC +// https://www.iana.org/domains/root/db/travel.html +travel + +// travelers : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/travelers.html +travelers + +// travelersinsurance : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/travelersinsurance.html +travelersinsurance + +// trust : Internet Naming Company LLC +// https://www.iana.org/domains/root/db/trust.html +trust + +// trv : Travelers TLD, LLC +// https://www.iana.org/domains/root/db/trv.html +trv + +// tube : Latin American Telecom LLC +// https://www.iana.org/domains/root/db/tube.html +tube + +// tui : TUI AG +// https://www.iana.org/domains/root/db/tui.html +tui + +// tunes : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/tunes.html +tunes + +// tushu : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/tushu.html +tushu + +// tvs : T V SUNDRAM IYENGAR & SONS LIMITED +// https://www.iana.org/domains/root/db/tvs.html +tvs + +// ubank : National Australia Bank Limited +// https://www.iana.org/domains/root/db/ubank.html +ubank + +// ubs : UBS AG +// https://www.iana.org/domains/root/db/ubs.html +ubs + +// unicom : China United Network Communications Corporation Limited +// https://www.iana.org/domains/root/db/unicom.html +unicom + +// university : Binky Moon, LLC +// https://www.iana.org/domains/root/db/university.html +university + +// uno : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/uno.html +uno + +// uol : UBN INTERNET LTDA. +// https://www.iana.org/domains/root/db/uol.html +uol + +// ups : UPS Market Driver, Inc. +// https://www.iana.org/domains/root/db/ups.html +ups + +// vacations : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vacations.html +vacations + +// vana : D3 Registry LLC +// https://www.iana.org/domains/root/db/vana.html +vana + +// vanguard : The Vanguard Group, Inc. +// https://www.iana.org/domains/root/db/vanguard.html +vanguard + +// vegas : Dot Vegas, Inc. +// https://www.iana.org/domains/root/db/vegas.html +vegas + +// ventures : Binky Moon, LLC +// https://www.iana.org/domains/root/db/ventures.html +ventures + +// verisign : VeriSign, Inc. +// https://www.iana.org/domains/root/db/verisign.html +verisign + +// versicherung : tldbox GmbH +// https://www.iana.org/domains/root/db/versicherung.html +versicherung + +// vet : Dog Beach, LLC +// https://www.iana.org/domains/root/db/vet.html +vet + +// viajes : Binky Moon, LLC +// https://www.iana.org/domains/root/db/viajes.html +viajes + +// video : Dog Beach, LLC +// https://www.iana.org/domains/root/db/video.html +video + +// vig : VIENNA INSURANCE GROUP AG Wiener Versicherung Gruppe +// https://www.iana.org/domains/root/db/vig.html +vig + +// viking : Viking River Cruises (Bermuda) Ltd. +// https://www.iana.org/domains/root/db/viking.html +viking + +// villas : Binky Moon, LLC +// https://www.iana.org/domains/root/db/villas.html +villas + +// vin : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vin.html +vin + +// vip : Registry Services, LLC +// https://www.iana.org/domains/root/db/vip.html +vip + +// virgin : Virgin Enterprises Limited +// https://www.iana.org/domains/root/db/virgin.html +virgin + +// visa : Visa Worldwide Pte. Limited +// https://www.iana.org/domains/root/db/visa.html +visa + +// vision : Binky Moon, LLC +// https://www.iana.org/domains/root/db/vision.html +vision + +// viva : Saudi Telecom Company +// https://www.iana.org/domains/root/db/viva.html +viva + +// vivo : Telefonica Brasil S.A. +// https://www.iana.org/domains/root/db/vivo.html +vivo + +// vlaanderen : DNS.be vzw +// https://www.iana.org/domains/root/db/vlaanderen.html +vlaanderen + +// vodka : Registry Services, LLC +// https://www.iana.org/domains/root/db/vodka.html +vodka + +// volvo : Volvo Holding Sverige Aktiebolag +// https://www.iana.org/domains/root/db/volvo.html +volvo + +// vote : Monolith Registry LLC +// https://www.iana.org/domains/root/db/vote.html +vote + +// voting : Valuetainment Corp. +// https://www.iana.org/domains/root/db/voting.html +voting + +// voto : Monolith Registry LLC +// https://www.iana.org/domains/root/db/voto.html +voto + +// voyage : Binky Moon, LLC +// https://www.iana.org/domains/root/db/voyage.html +voyage + +// wales : Nominet UK +// https://www.iana.org/domains/root/db/wales.html +wales + +// walmart : Wal-Mart Stores, Inc. +// https://www.iana.org/domains/root/db/walmart.html +walmart + +// walter : Sandvik AB +// https://www.iana.org/domains/root/db/walter.html +walter + +// wang : Zodiac Wang Limited +// https://www.iana.org/domains/root/db/wang.html +wang + +// wanggou : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/wanggou.html +wanggou + +// watch : Binky Moon, LLC +// https://www.iana.org/domains/root/db/watch.html +watch + +// watches : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/watches.html +watches + +// weather : The Weather Company, LLC +// https://www.iana.org/domains/root/db/weather.html +weather + +// weatherchannel : The Weather Company, LLC +// https://www.iana.org/domains/root/db/weatherchannel.html +weatherchannel + +// webcam : dot Webcam Limited +// https://www.iana.org/domains/root/db/webcam.html +webcam + +// weber : Saint-Gobain Weber SA +// https://www.iana.org/domains/root/db/weber.html +weber + +// website : Radix Technologies Inc SEZC +// https://www.iana.org/domains/root/db/website.html +website + +// wed +// https://www.iana.org/domains/root/db/wed.html +wed + +// wedding : Registry Services, LLC +// https://www.iana.org/domains/root/db/wedding.html +wedding + +// weibo : Sina Corporation +// https://www.iana.org/domains/root/db/weibo.html +weibo + +// weir : Weir Group IP Limited +// https://www.iana.org/domains/root/db/weir.html +weir + +// whoswho : Who's Who Registry +// https://www.iana.org/domains/root/db/whoswho.html +whoswho + +// wien : punkt.wien GmbH +// https://www.iana.org/domains/root/db/wien.html +wien + +// wiki : Registry Services, LLC +// https://www.iana.org/domains/root/db/wiki.html +wiki + +// williamhill : William Hill Organization Limited +// https://www.iana.org/domains/root/db/williamhill.html +williamhill + +// win : First Registry Limited +// https://www.iana.org/domains/root/db/win.html +win + +// windows : Microsoft Corporation +// https://www.iana.org/domains/root/db/windows.html +windows + +// wine : Binky Moon, LLC +// https://www.iana.org/domains/root/db/wine.html +wine + +// winners : The TJX Companies, Inc. +// https://www.iana.org/domains/root/db/winners.html +winners + +// wme : William Morris Endeavor Entertainment, LLC +// https://www.iana.org/domains/root/db/wme.html +wme + +// wolterskluwer : Wolters Kluwer N.V. +// https://www.iana.org/domains/root/db/wolterskluwer.html +wolterskluwer + +// woodside : Woodside Petroleum Limited +// https://www.iana.org/domains/root/db/woodside.html +woodside + +// work : Registry Services, LLC +// https://www.iana.org/domains/root/db/work.html +work + +// works : Binky Moon, LLC +// https://www.iana.org/domains/root/db/works.html +works + +// world : Binky Moon, LLC +// https://www.iana.org/domains/root/db/world.html +world + +// wow : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/wow.html +wow + +// wtc : World Trade Centers Association, Inc. +// https://www.iana.org/domains/root/db/wtc.html +wtc + +// wtf : Binky Moon, LLC +// https://www.iana.org/domains/root/db/wtf.html +wtf + +// xbox : Microsoft Corporation +// https://www.iana.org/domains/root/db/xbox.html +xbox + +// xerox : Xerox DNHC LLC +// https://www.iana.org/domains/root/db/xerox.html +xerox + +// xihuan : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/xihuan.html +xihuan + +// xin : Elegant Leader Limited +// https://www.iana.org/domains/root/db/xin.html +xin + +// xn--11b4c3d : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--11b4c3d.html +कॉम + +// xn--1ck2e1b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--1ck2e1b.html +セール + +// xn--1qqw23a : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--1qqw23a.html +佛山 + +// xn--30rr7y : Excellent First Limited +// https://www.iana.org/domains/root/db/xn--30rr7y.html +慈善 + +// xn--3bst00m : Eagle Horizon Limited +// https://www.iana.org/domains/root/db/xn--3bst00m.html +集团 + +// xn--3ds443g : Beijing TLD Registry Technology Limited +// https://www.iana.org/domains/root/db/xn--3ds443g.html +在线 + +// xn--3pxu8k : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--3pxu8k.html +点看 + +// xn--42c2d9a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--42c2d9a.html +คอม + +// xn--45q11c : Zodiac Gemini Ltd +// https://www.iana.org/domains/root/db/xn--45q11c.html +八卦 + +// xn--4gbrim : Helium TLDs Ltd +// https://www.iana.org/domains/root/db/xn--4gbrim.html +موقع + +// xn--55qw42g : China Organizational Name Administration Center +// https://www.iana.org/domains/root/db/xn--55qw42g.html +公益 + +// xn--55qx5d : China Internet Network Information Center (CNNIC) +// https://www.iana.org/domains/root/db/xn--55qx5d.html +公司 + +// xn--5su34j936bgsg : Shangri‐La International Hotel Management Limited +// https://www.iana.org/domains/root/db/xn--5su34j936bgsg.html +香格里拉 + +// xn--5tzm5g : Global Website TLD Asia Limited +// https://www.iana.org/domains/root/db/xn--5tzm5g.html +网站 + +// xn--6frz82g : Identity Digital Domains Limited +// https://www.iana.org/domains/root/db/xn--6frz82g.html +移动 + +// xn--6qq986b3xl : Tycoon Treasure Limited +// https://www.iana.org/domains/root/db/xn--6qq986b3xl.html +我爱你 + +// xn--80adxhks : Foundation for Assistance for Internet Technologies and Infrastructure Development (FAITID) +// https://www.iana.org/domains/root/db/xn--80adxhks.html +москва + +// xn--80aqecdr1a : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--80aqecdr1a.html +католик + +// xn--80asehdb : CORE Association +// https://www.iana.org/domains/root/db/xn--80asehdb.html +онлайн + +// xn--80aswg : CORE Association +// https://www.iana.org/domains/root/db/xn--80aswg.html +сайт + +// xn--8y0a063a : China United Network Communications Corporation Limited +// https://www.iana.org/domains/root/db/xn--8y0a063a.html +联通 + +// xn--9dbq2a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--9dbq2a.html +קום + +// xn--9et52u : RISE VICTORY LIMITED +// https://www.iana.org/domains/root/db/xn--9et52u.html +时尚 + +// xn--9krt00a : Sina Corporation +// https://www.iana.org/domains/root/db/xn--9krt00a.html +微博 + +// xn--b4w605ferd : Temasek Holdings (Private) Limited +// https://www.iana.org/domains/root/db/xn--b4w605ferd.html +淡马锡 + +// xn--bck1b9a5dre4c : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--bck1b9a5dre4c.html +ファッション + +// xn--c1avg : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--c1avg.html +орг + +// xn--c2br7g : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--c2br7g.html +नेट + +// xn--cck2b3b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--cck2b3b.html +ストア + +// xn--cckwcxetd : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--cckwcxetd.html +アマゾン + +// xn--cg4bki : SAMSUNG SDS CO., LTD +// https://www.iana.org/domains/root/db/xn--cg4bki.html +삼성 + +// xn--czr694b : Internet DotTrademark Organisation Limited +// https://www.iana.org/domains/root/db/xn--czr694b.html +商标 + +// xn--czrs0t : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--czrs0t.html +商店 + +// xn--czru2d : Zodiac Aquarius Limited +// https://www.iana.org/domains/root/db/xn--czru2d.html +商城 + +// xn--d1acj3b : The Foundation for Network Initiatives “The Smart Internet” +// https://www.iana.org/domains/root/db/xn--d1acj3b.html +дети + +// xn--eckvdtc9d : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--eckvdtc9d.html +ポイント + +// xn--efvy88h : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--efvy88h.html +新闻 + +// xn--fct429k : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--fct429k.html +家電 + +// xn--fhbei : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--fhbei.html +كوم + +// xn--fiq228c5hs : Beijing TLD Registry Technology Limited +// https://www.iana.org/domains/root/db/xn--fiq228c5hs.html +中文网 + +// xn--fiq64b : CITIC Group Corporation +// https://www.iana.org/domains/root/db/xn--fiq64b.html +中信 + +// xn--fjq720a : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--fjq720a.html +娱乐 + +// xn--flw351e : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--flw351e.html +谷歌 + +// xn--fzys8d69uvgm : PCCW Enterprises Limited +// https://www.iana.org/domains/root/db/xn--fzys8d69uvgm.html +電訊盈科 + +// xn--g2xx48c : Nawang Heli(Xiamen) Network Service Co., LTD. +// https://www.iana.org/domains/root/db/xn--g2xx48c.html +购物 + +// xn--gckr3f0f : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--gckr3f0f.html +クラウド + +// xn--gk3at1e : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--gk3at1e.html +通販 + +// xn--hxt814e : Zodiac Taurus Limited +// https://www.iana.org/domains/root/db/xn--hxt814e.html +网店 + +// xn--i1b6b1a6a2e : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--i1b6b1a6a2e.html +संगठन + +// xn--imr513n : Internet DotTrademark Organisation Limited +// https://www.iana.org/domains/root/db/xn--imr513n.html +餐厅 + +// xn--io0a7i : China Internet Network Information Center (CNNIC) +// https://www.iana.org/domains/root/db/xn--io0a7i.html +网络 + +// xn--j1aef : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--j1aef.html +ком + +// xn--jlq480n2rg : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--jlq480n2rg.html +亚马逊 + +// xn--jvr189m : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--jvr189m.html +食品 + +// xn--kcrx77d1x4a : Koninklijke Philips N.V. +// https://www.iana.org/domains/root/db/xn--kcrx77d1x4a.html +飞利浦 + +// xn--kput3i : Beijing RITT-Net Technology Development Co., Ltd +// https://www.iana.org/domains/root/db/xn--kput3i.html +手机 + +// xn--mgba3a3ejt : Aramco Services Company +// https://www.iana.org/domains/root/db/xn--mgba3a3ejt.html +ارامكو + +// xn--mgba7c0bbn0a : Competrol (Luxembourg) Sarl +// https://www.iana.org/domains/root/db/xn--mgba7c0bbn0a.html +العليان + +// xn--mgbab2bd : CORE Association +// https://www.iana.org/domains/root/db/xn--mgbab2bd.html +بازار + +// xn--mgbca7dzdo : Abu Dhabi Systems and Information Centre +// https://www.iana.org/domains/root/db/xn--mgbca7dzdo.html +ابوظبي + +// xn--mgbi4ecexp : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--mgbi4ecexp.html +كاثوليك + +// xn--mgbt3dhd +// https://www.iana.org/domains/root/db/xn--mgbt3dhd.html +همراه + +// xn--mk1bu44c : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--mk1bu44c.html +닷컴 + +// xn--mxtq1m : Net-Chinese Co., Ltd. +// https://www.iana.org/domains/root/db/xn--mxtq1m.html +政府 + +// xn--ngbc5azd : International Domain Registry Pty. Ltd. +// https://www.iana.org/domains/root/db/xn--ngbc5azd.html +شبكة + +// xn--ngbe9e0a : Kuwait Finance House +// https://www.iana.org/domains/root/db/xn--ngbe9e0a.html +بيتك + +// xn--ngbrx : League of Arab States +// https://www.iana.org/domains/root/db/xn--ngbrx.html +عرب + +// xn--nqv7f : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--nqv7f.html +机构 + +// xn--nqv7fs00ema : Public Interest Registry +// https://www.iana.org/domains/root/db/xn--nqv7fs00ema.html +组织机构 + +// xn--nyqy26a : Stable Tone Limited +// https://www.iana.org/domains/root/db/xn--nyqy26a.html +健康 + +// xn--otu796d : Jiang Yu Liang Cai Technology Company Limited +// https://www.iana.org/domains/root/db/xn--otu796d.html +招聘 + +// xn--p1acf : Rusnames Limited +// https://www.iana.org/domains/root/db/xn--p1acf.html +рус + +// xn--pssy2u : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--pssy2u.html +大拿 + +// xn--q9jyb4c : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--q9jyb4c.html +みんな + +// xn--qcka1pmc : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/xn--qcka1pmc.html +グーグル + +// xn--rhqv96g : Stable Tone Limited +// https://www.iana.org/domains/root/db/xn--rhqv96g.html +世界 + +// xn--rovu88b : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/xn--rovu88b.html +書籍 + +// xn--ses554g : KNET Co., Ltd. +// https://www.iana.org/domains/root/db/xn--ses554g.html +网址 + +// xn--t60b56a : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--t60b56a.html +닷넷 + +// xn--tckwe : VeriSign Sarl +// https://www.iana.org/domains/root/db/xn--tckwe.html +コム + +// xn--tiq49xqyj : Pontificium Consilium de Comunicationibus Socialibus (PCCS) (Pontifical Council for Social Communication) +// https://www.iana.org/domains/root/db/xn--tiq49xqyj.html +天主教 + +// xn--unup4y : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--unup4y.html +游戏 + +// xn--vermgensberater-ctb : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/xn--vermgensberater-ctb.html +vermögensberater + +// xn--vermgensberatung-pwb : Deutsche Vermögensberatung Aktiengesellschaft DVAG +// https://www.iana.org/domains/root/db/xn--vermgensberatung-pwb.html +vermögensberatung + +// xn--vhquv : Binky Moon, LLC +// https://www.iana.org/domains/root/db/xn--vhquv.html +企业 + +// xn--vuq861b : Beijing Tele-info Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--vuq861b.html +信息 + +// xn--w4r85el8fhu5dnra : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/xn--w4r85el8fhu5dnra.html +嘉里大酒店 + +// xn--w4rs40l : Kerry Trading Co. Limited +// https://www.iana.org/domains/root/db/xn--w4rs40l.html +嘉里 + +// xn--xhq521b : Guangzhou YU Wei Information Technology Co., Ltd. +// https://www.iana.org/domains/root/db/xn--xhq521b.html +广东 + +// xn--zfr164b : China Organizational Name Administration Center +// https://www.iana.org/domains/root/db/xn--zfr164b.html +政务 + +// xyz : XYZ.COM LLC +// https://www.iana.org/domains/root/db/xyz.html +xyz + +// yachts : XYZ.COM LLC +// https://www.iana.org/domains/root/db/yachts.html +yachts + +// yahoo : Yahoo Inc. +// https://www.iana.org/domains/root/db/yahoo.html +yahoo + +// yamaxun : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/yamaxun.html +yamaxun + +// yandex : YANDEX, LLC +// https://www.iana.org/domains/root/db/yandex.html +yandex + +// yodobashi : YODOBASHI CAMERA CO.,LTD. +// https://www.iana.org/domains/root/db/yodobashi.html +yodobashi + +// yoga : Registry Services, LLC +// https://www.iana.org/domains/root/db/yoga.html +yoga + +// yokohama : GMO Registry, Inc. +// https://www.iana.org/domains/root/db/yokohama.html +yokohama + +// you : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/you.html +you + +// youtube : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/youtube.html +youtube + +// yun : Beijing Qihu Keji Co., Ltd. +// https://www.iana.org/domains/root/db/yun.html +yun + +// zappos : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/zappos.html +zappos + +// zara : Industria de Diseño Textil, S.A. (INDITEX, S.A.) +// https://www.iana.org/domains/root/db/zara.html +zara + +// zero : Amazon Registry Services, Inc. +// https://www.iana.org/domains/root/db/zero.html +zero + +// zip : Charleston Road Registry Inc. +// https://www.iana.org/domains/root/db/zip.html +zip + +// zone : Binky Moon, LLC +// https://www.iana.org/domains/root/db/zone.html +zone + +// zuerich : Kanton Zürich (Canton of Zurich) +// https://www.iana.org/domains/root/db/zuerich.html +zuerich + +// ===END ICANN DOMAINS=== diff --git a/client/src/test/java/org/asynchttpclient/cookie/PublicSuffixCookieTest.java b/client/src/test/java/org/asynchttpclient/cookie/PublicSuffixCookieTest.java new file mode 100644 index 0000000000..7abe39f67e --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/cookie/PublicSuffixCookieTest.java @@ -0,0 +1,125 @@ +/* + * 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.cookie; + +import io.netty.handler.codec.http.cookie.DefaultCookie; +import org.asynchttpclient.uri.Uri; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Locale; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * RFC 6265 Section 5.3 step 5: a cookie whose {@code Domain} names a public suffix must be ignored. + * Without it, one site under a registry can plant a cookie that every other site under that registry + * receives. Step 6, which asks only whether the request host sits under the Domain, does not catch it. + */ +public class PublicSuffixCookieTest { + + private static boolean reaches(String setterHost, String domainAttribute, String victimHost) { + ThreadSafeCookieStore store = new ThreadSafeCookieStore(); + DefaultCookie cookie = new DefaultCookie("SID", "planted"); + if (domainAttribute != null) { + cookie.setDomain(domainAttribute); + } + cookie.setPath("/"); + store.add(Uri.create("http://" + setterHost + "/"), cookie); + + List got = store.get(Uri.create("http://" + victimHost + "/")); + return got.stream().anyMatch(c -> "SID".equals(c.name())); + } + + @Test + public void aCookieForAPublicSuffixIsNotPlanted() { + assertFalse(reaches("evil.co.uk", "co.uk", "bank.co.uk"), + "a host under co.uk must not set a cookie for co.uk itself"); + assertFalse(reaches("evil.co.uk", "uk", "bank.co.uk"), + "a host must not set a cookie for a bare TLD"); + assertFalse(reaches("evil.com", "com", "bank.com"), + "a host must not set a cookie for com"); + } + + /** + * The existing cookie-tossing guard must keep working: this is the case #2196 fixed, and it is what + * shows the new check is not the only thing standing between these two hosts. + */ + @Test + public void theExistingCrossSiteGuardStillHolds() { + assertFalse(reaches("evil.co.uk", "bank.co.uk", "bank.co.uk"), + "one host must not set a cookie naming an unrelated host"); + } + + /** + * Ordinary cookies must be unaffected, or every user loses their session handling. + */ + @Test + public void ordinaryCookiesAreUnaffected() { + assertTrue(reaches("bank.co.uk", "bank.co.uk", "www.bank.co.uk"), + "a site must still set a cookie for its own registrable domain"); + assertTrue(reaches("bank.co.uk", null, "bank.co.uk"), + "a host-only cookie must still be returned to that host"); + assertTrue(reaches("www.example.com", "example.com", "api.example.com"), + "a site must still share a cookie across its own subdomains"); + } + + /** + * RFC 6265 section 5.3 step 5 keeps a cookie whose Domain equals the request host, as a host-only + * cookie. Dropping it would break ordinary single-label hosts, because dev, app, box, cloud and a + * dozen more are ICANN suffixes as well as the short names Docker Compose and Kubernetes hand out. + */ + @Test + public void aSingleLabelHostCanStillSetItsOwnCookie() { + for (String host : new String[]{"dev", "app", "box", "cloud", "build", "run"}) { + assertTrue(PublicSuffixList.isPublicSuffix(host), host + " is expected to be an ICANN suffix"); + assertTrue(reaches(host, host, host), + "a host whose own name is a public suffix must still set a cookie for itself: " + host); + } + } + + /** + * The locale must not decide whether the check engages. Under Turkish the default lowercasing turns I + * into a dotless i, so every I-initial suffix would stop matching and the guard would fail open. + */ + @Test + public void matchingDoesNotDependOnTheDefaultLocale() { + Locale original = Locale.getDefault(); + try { + Locale.setDefault(new Locale("tr", "TR")); + assertTrue(PublicSuffixList.isPublicSuffix("INFO"), "INFO must match under a Turkish locale"); + assertTrue(PublicSuffixList.isPublicSuffix("CO.IN"), "CO.IN must match under a Turkish locale"); + assertFalse(reaches("evil.co.in", "co.in", "bank.co.in"), + "the guard must hold under a Turkish locale"); + } finally { + Locale.setDefault(original); + } + } + + @Test + public void theListRecognisesSuffixesAndRegistrableDomains() { + assertTrue(PublicSuffixList.isPublicSuffix("co.uk")); + assertTrue(PublicSuffixList.isPublicSuffix("com")); + assertTrue(PublicSuffixList.isPublicSuffix("com.au")); + assertTrue(PublicSuffixList.isPublicSuffix("CO.UK"), "matching must be case-insensitive"); + + assertFalse(PublicSuffixList.isPublicSuffix("bank.co.uk")); + assertFalse(PublicSuffixList.isPublicSuffix("example.com")); + assertFalse(PublicSuffixList.isPublicSuffix("")); + assertFalse(PublicSuffixList.isPublicSuffix(null)); + } +}