Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Apache HTTP Client 5",
"contributor": "",
"description": "Fail fast at Apache5HttpClient construction when SecurityManager is active and jdk.net.NetworkPermission setOption.TCP_KEEPIDLE, setOption.TCP_KEEPINTERVAL, setOption.TCP_KEEPCOUNT are not granted."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. 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.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.apache;

import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientSecurityManagerTestSuite;

@EnabledForJreRange(max = JRE.JAVA_17)
class ApacheSecurityManagerHttpCallTest extends SdkHttpClientSecurityManagerTestSuite {

@Override
protected SdkHttpClient createHttpClient() {
return ApacheHttpClient.builder().build();
}

@Override
protected String getPolicyFileUrl() {
return getClass().getResource("security-manager-test.policy").toExternalForm();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
grant {
permission java.util.PropertyPermission "*", "read,write";
permission java.lang.RuntimePermission "modifyThread";
permission java.lang.RuntimePermission "setContextClassLoader";
permission java.lang.RuntimePermission "setSecurityManager";
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.net.SocketPermission "*", "connect,accept";
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@
import java.net.InetAddress;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -101,6 +103,7 @@
import software.amazon.awssdk.metrics.MetricCollector;
import software.amazon.awssdk.metrics.NoOpMetricCollector;
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.ClassLoaderHelper;
import software.amazon.awssdk.utils.Logger;
import software.amazon.awssdk.utils.Validate;

Expand Down Expand Up @@ -543,6 +546,12 @@ public interface Builder extends SdkHttpClient.Builder<Apache5HttpClient.Builder
}

private static final class DefaultBuilder implements Builder {
private static final String[] REQUIRED_TCP_SOCKET_OPTION_PERMISSIONS = {
"setOption.TCP_KEEPIDLE",
"setOption.TCP_KEEPINTERVAL",
"setOption.TCP_KEEPCOUNT"
};

private final AttributeMap.Builder standardOptions = AttributeMap.builder();
private Registry<AuthSchemeFactory> authSchemeRegistry;
private ProxyConfiguration proxyConfiguration = ProxyConfiguration.builder().build();
Expand Down Expand Up @@ -744,8 +753,46 @@ public void setAuthSchemeProviderRegistry(Registry<AuthSchemeFactory> authScheme
public SdkHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
AttributeMap resolvedOptions = standardOptions.build().merge(serviceDefaults).merge(
SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS);
checkTcpSocketOptionPermissions();
return new Apache5HttpClient(this, resolvedOptions);
}

/**
* Fails fast if a SecurityManager is active but denies the {@code jdk.net.NetworkPermission} entries
* that Apache HC5 requires for its default TCP keepalive socket options.
* No-op when no SecurityManager is installed (including Java 24+).
*/
private static void checkTcpSocketOptionPermissions() {
SecurityManager sm = System.getSecurityManager();
if (sm == null) {
return;
}

try {
Class<?> permClass = ClassLoaderHelper.loadClass("jdk.net.NetworkPermission", Apache5HttpClient.class);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that NetworkPermission is marked for forRemoval in Java 25 thus loading it from ClassLoaderHelper to prevent ClassNotFoundException for future Java versions.
https://docs.oracle.com/en/java/javase/26/docs/api/jdk.net/jdk/net/NetworkPermission.html

for (String permName : REQUIRED_TCP_SOCKET_OPTION_PERMISSIONS) {
Permission perm = (Permission) permClass.getConstructor(String.class).newInstance(permName);
sm.checkPermission(perm);
}
} catch (SecurityException e) {
if (isTcpSocketOptionPermissionDenied(e)) {
throw new IllegalStateException(
"Apache5HttpClient requires jdk.net.NetworkPermission for \""
+ String.join("\", \"", REQUIRED_TCP_SOCKET_OPTION_PERMISSIONS)
+ "\" when a SecurityManager is active.", e);
}
log.debug(() -> "SecurityManager denied a non-TCP socket option permission during verification: "
+ e.getMessage(), e);
} catch (Exception e) {
log.debug(() -> "Could not verify jdk.net.NetworkPermission for TCP socket options: " + e.getMessage(), e);
}
}

private static boolean isTcpSocketOptionPermissionDenied(SecurityException securityException) {
String message = securityException.getMessage();
return message != null && Arrays.stream(REQUIRED_TCP_SOCKET_OPTION_PERMISSIONS).anyMatch(message::contains);
}

}

private static class ApacheConnectionManagerFactory {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. 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.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.apache5;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.security.Permission;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.logging.log4j.Level;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.awssdk.testutils.LogCaptor;

/**
* Tests that Apache5HttpClient fails fast at construction time when a SecurityManager
* denies jdk.net.NetworkPermission for TCP keepalive extended options.
*/
@EnabledForJreRange(max = JRE.JAVA_17)
class Apache5SecurityManagerClientCreationTest {

@AfterEach
void tearDown() {
System.setSecurityManager(null);
System.clearProperty("java.security.policy");
java.security.Policy.getPolicy().refresh();
}

@Test
void buildWithDefaults_whenStandardPermissionsGrantedButNetworkPermissionMissing_shouldThrowIllegalStateException() {
System.setProperty("java.security.policy", "=" + getPolicyUrl());
java.security.Policy.getPolicy().refresh();
System.setSecurityManager(new SecurityManager());

assertThatThrownBy(() -> Apache5HttpClient.builder().build())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("jdk.net.NetworkPermission");
}

private String getPolicyUrl() {
return getClass().getResource("security-manager-test.policy").toExternalForm();
}

@Test
void buildWithDefaults_whenUnrelatedSecurityExceptionThrown_shouldNotThrow() {
System.setSecurityManager(new SecurityManager() {
@Override
public void checkPermission(Permission perm) {
if ("jdk.net.NetworkPermission".equals(perm.getClass().getName())) {
throw new SecurityException("access denied: some.unrelated.permission");
}
}
});

try (LogCaptor logCaptor = LogCaptor.create(Level.DEBUG)) {
assertThatNoException().isThrownBy(() -> {
Apache5HttpClient.builder().build().close();
});
assertThat(logCaptor.loggedEvents()).anySatisfy(logEvent -> {
assertThat(logEvent.getLevel()).isEqualTo(Level.DEBUG);
assertThat(logEvent.getMessage().getFormattedMessage())
.contains("SecurityManager denied a non-TCP socket option permission");
});
}
}

@ParameterizedTest
@MethodSource("partiallyGrantedPermissions")
void buildWithDefaults_whenNotAllPermissionsGranted_shouldThrowIllegalStateException(Set<String> grantedPermissions) {
System.setSecurityManager(new GrantOnlyNetworkPermissionSecurityManager(grantedPermissions));

assertThatThrownBy(() -> Apache5HttpClient.builder().build())
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("jdk.net.NetworkPermission");
}

@Test
void buildWithDefaults_whenAllPermissionsGranted_shouldSucceed() {
Set<String> allGranted = new HashSet<>(Arrays.asList(
"setOption.TCP_KEEPIDLE", "setOption.TCP_KEEPINTERVAL", "setOption.TCP_KEEPCOUNT"));
System.setSecurityManager(new GrantOnlyNetworkPermissionSecurityManager(allGranted));
assertThatNoException().isThrownBy(() -> {
Apache5HttpClient.builder().build().close();
});
}

@Test
void buildWithDefaults_whenNoSecurityManager_shouldSucceed() {
assertThatNoException().isThrownBy(() -> {
Apache5HttpClient.builder().build().close();
});
}

static Stream<Arguments> partiallyGrantedPermissions() {
return Stream.of(
// 0 out of 3 granted
Arguments.of(new HashSet<>()),
// 1 out of 3 granted
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPIDLE"))),
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPINTERVAL"))),
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPCOUNT"))),
// 2 out of 3 granted
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPIDLE", "setOption.TCP_KEEPINTERVAL"))),
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPIDLE", "setOption.TCP_KEEPCOUNT"))),
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPINTERVAL", "setOption.TCP_KEEPCOUNT")))
);
}

/**
* SecurityManager that only grants specific jdk.net.NetworkPermission entries and denies the rest.
*/
private static class GrantOnlyNetworkPermissionSecurityManager extends SecurityManager {
private final Set<String> grantedPermissions;

GrantOnlyNetworkPermissionSecurityManager(Set<String> grantedPermissions) {
this.grantedPermissions = grantedPermissions;
}

@Override
public void checkPermission(Permission perm) {
if ("jdk.net.NetworkPermission".equals(perm.getClass().getName())
&& !grantedPermissions.contains(perm.getName())) {
throw new SecurityException("Denied: " + perm.getName());
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. 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.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.apache5;

import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import software.amazon.awssdk.http.SdkHttpClient;
import software.amazon.awssdk.http.SdkHttpClientSecurityManagerTestSuite;

@EnabledForJreRange(max = JRE.JAVA_17)
class Apache5SecurityManagerHttpCallTest extends SdkHttpClientSecurityManagerTestSuite {

@Override
protected SdkHttpClient createHttpClient() {
return Apache5HttpClient.builder().build();
}

@Override
protected String getPolicyFileUrl() {
return getClass().getResource("security-manager-test-with-http-call.policy").toExternalForm();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
grant {
permission java.util.PropertyPermission "*", "read,write";
permission java.lang.RuntimePermission "modifyThread";
permission java.lang.RuntimePermission "setContextClassLoader";
permission java.lang.RuntimePermission "setSecurityManager";
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.net.SocketPermission "*", "connect,accept";

// Required by Apache HC5 for TCP socket options (not needed by Apache HC4)
permission jdk.net.NetworkPermission "setOption.TCP_KEEPIDLE";
permission jdk.net.NetworkPermission "setOption.TCP_KEEPINTERVAL";
permission jdk.net.NetworkPermission "setOption.TCP_KEEPCOUNT";
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
grant {
permission java.util.PropertyPermission "*", "read,write";
permission java.io.FilePermission "<<ALL FILES>>", "read,write";
permission java.lang.RuntimePermission "getenv.*";
permission "java.lang.RuntimePermission" "accessDeclaredMembers";
permission "javax.net.ssl.SSLPermission" "setDefaultSSLContext";
permission "java.net.SocketPermission" "*", "connect,resolve";

// Needed for test to remove the security manager
permission java.lang.RuntimePermission "setSecurityManager";

// jdk.net.NetworkPermission for setOption.TCP_KEEPIDLE, setOption.TCP_KEEPINTERVAL,
// setOption.TCP_KEEPCOUNT is explicitly NOT granted to test that Apache5HttpClient
// fails fast when these permissions are missing.
};
Loading
Loading