Skip to content

Commit e00b85e

Browse files
authored
[apache5-client] Fail fast when SecurityManager lacks TCP_KEEPIDLE/TCP_KEEPINTERVAL/TCP_KEEPCOUNT permissions. (#6992)
* 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 * Update Junits * update junit test cases and handle case where we can get security exception other than one expected * Update for loop * Handle PR comemnt to add test case with actual request * checkstyle * Handle review comment
1 parent 334f7bd commit e00b85e

9 files changed

Lines changed: 404 additions & 0 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Apache HTTP Client 5",
4+
"contributor": "",
5+
"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."
6+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.apache;
17+
18+
import org.junit.jupiter.api.condition.EnabledForJreRange;
19+
import org.junit.jupiter.api.condition.JRE;
20+
import software.amazon.awssdk.http.SdkHttpClient;
21+
import software.amazon.awssdk.http.SdkHttpClientSecurityManagerTestSuite;
22+
23+
@EnabledForJreRange(max = JRE.JAVA_17)
24+
class ApacheSecurityManagerHttpCallTest extends SdkHttpClientSecurityManagerTestSuite {
25+
26+
@Override
27+
protected SdkHttpClient createHttpClient() {
28+
return ApacheHttpClient.builder().build();
29+
}
30+
31+
@Override
32+
protected String getPolicyFileUrl() {
33+
return getClass().getResource("security-manager-test.policy").toExternalForm();
34+
}
35+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
grant {
2+
permission java.util.PropertyPermission "*", "read,write";
3+
permission java.lang.RuntimePermission "modifyThread";
4+
permission java.lang.RuntimePermission "setContextClassLoader";
5+
permission java.lang.RuntimePermission "setSecurityManager";
6+
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
7+
permission java.net.SocketPermission "*", "connect,accept";
8+
};

http-clients/apache5-client/src/main/java/software/amazon/awssdk/http/apache5/Apache5HttpClient.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@
2828
import java.net.InetAddress;
2929
import java.security.KeyManagementException;
3030
import java.security.NoSuchAlgorithmException;
31+
import java.security.Permission;
3132
import java.security.cert.CertificateException;
3233
import java.security.cert.X509Certificate;
3334
import java.time.Duration;
35+
import java.util.Arrays;
3436
import java.util.Iterator;
3537
import java.util.Optional;
3638
import java.util.concurrent.TimeUnit;
@@ -101,6 +103,7 @@
101103
import software.amazon.awssdk.metrics.MetricCollector;
102104
import software.amazon.awssdk.metrics.NoOpMetricCollector;
103105
import software.amazon.awssdk.utils.AttributeMap;
106+
import software.amazon.awssdk.utils.ClassLoaderHelper;
104107
import software.amazon.awssdk.utils.Logger;
105108
import software.amazon.awssdk.utils.Validate;
106109

@@ -543,6 +546,12 @@ public interface Builder extends SdkHttpClient.Builder<Apache5HttpClient.Builder
543546
}
544547

545548
private static final class DefaultBuilder implements Builder {
549+
private static final String[] REQUIRED_TCP_SOCKET_OPTION_PERMISSIONS = {
550+
"setOption.TCP_KEEPIDLE",
551+
"setOption.TCP_KEEPINTERVAL",
552+
"setOption.TCP_KEEPCOUNT"
553+
};
554+
546555
private final AttributeMap.Builder standardOptions = AttributeMap.builder();
547556
private Registry<AuthSchemeFactory> authSchemeRegistry;
548557
private ProxyConfiguration proxyConfiguration = ProxyConfiguration.builder().build();
@@ -744,8 +753,46 @@ public void setAuthSchemeProviderRegistry(Registry<AuthSchemeFactory> authScheme
744753
public SdkHttpClient buildWithDefaults(AttributeMap serviceDefaults) {
745754
AttributeMap resolvedOptions = standardOptions.build().merge(serviceDefaults).merge(
746755
SdkHttpConfigurationOption.GLOBAL_HTTP_DEFAULTS);
756+
checkTcpSocketOptionPermissions();
747757
return new Apache5HttpClient(this, resolvedOptions);
748758
}
759+
760+
/**
761+
* Fails fast if a SecurityManager is active but denies the {@code jdk.net.NetworkPermission} entries
762+
* that Apache HC5 requires for its default TCP keepalive socket options.
763+
* No-op when no SecurityManager is installed (including Java 24+).
764+
*/
765+
private static void checkTcpSocketOptionPermissions() {
766+
SecurityManager sm = System.getSecurityManager();
767+
if (sm == null) {
768+
return;
769+
}
770+
771+
try {
772+
Class<?> permClass = ClassLoaderHelper.loadClass("jdk.net.NetworkPermission", Apache5HttpClient.class);
773+
for (String permName : REQUIRED_TCP_SOCKET_OPTION_PERMISSIONS) {
774+
Permission perm = (Permission) permClass.getConstructor(String.class).newInstance(permName);
775+
sm.checkPermission(perm);
776+
}
777+
} catch (SecurityException e) {
778+
if (isTcpSocketOptionPermissionDenied(e)) {
779+
throw new IllegalStateException(
780+
"Apache5HttpClient requires jdk.net.NetworkPermission for \""
781+
+ String.join("\", \"", REQUIRED_TCP_SOCKET_OPTION_PERMISSIONS)
782+
+ "\" when a SecurityManager is active.", e);
783+
}
784+
log.debug(() -> "SecurityManager denied a non-TCP socket option permission during verification: "
785+
+ e.getMessage(), e);
786+
} catch (Exception e) {
787+
log.debug(() -> "Could not verify jdk.net.NetworkPermission for TCP socket options: " + e.getMessage(), e);
788+
}
789+
}
790+
791+
private static boolean isTcpSocketOptionPermissionDenied(SecurityException securityException) {
792+
String message = securityException.getMessage();
793+
return message != null && Arrays.stream(REQUIRED_TCP_SOCKET_OPTION_PERMISSIONS).anyMatch(message::contains);
794+
}
795+
749796
}
750797

751798
private static class ApacheConnectionManagerFactory {
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.apache5;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatNoException;
20+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
22+
import java.security.Permission;
23+
import java.util.Arrays;
24+
import java.util.HashSet;
25+
import java.util.Set;
26+
import java.util.stream.Stream;
27+
import org.apache.logging.log4j.Level;
28+
import org.junit.jupiter.api.AfterEach;
29+
import org.junit.jupiter.api.Test;
30+
import org.junit.jupiter.api.condition.EnabledForJreRange;
31+
import org.junit.jupiter.api.condition.JRE;
32+
import org.junit.jupiter.params.ParameterizedTest;
33+
import org.junit.jupiter.params.provider.Arguments;
34+
import org.junit.jupiter.params.provider.MethodSource;
35+
import software.amazon.awssdk.testutils.LogCaptor;
36+
37+
/**
38+
* Tests that Apache5HttpClient fails fast at construction time when a SecurityManager
39+
* denies jdk.net.NetworkPermission for TCP keepalive extended options.
40+
*/
41+
@EnabledForJreRange(max = JRE.JAVA_17)
42+
class Apache5SecurityManagerClientCreationTest {
43+
44+
@AfterEach
45+
void tearDown() {
46+
System.setSecurityManager(null);
47+
System.clearProperty("java.security.policy");
48+
java.security.Policy.getPolicy().refresh();
49+
}
50+
51+
@Test
52+
void buildWithDefaults_whenStandardPermissionsGrantedButNetworkPermissionMissing_shouldThrowIllegalStateException() {
53+
System.setProperty("java.security.policy", "=" + getPolicyUrl());
54+
java.security.Policy.getPolicy().refresh();
55+
System.setSecurityManager(new SecurityManager());
56+
57+
assertThatThrownBy(() -> Apache5HttpClient.builder().build())
58+
.isInstanceOf(IllegalStateException.class)
59+
.hasMessageContaining("jdk.net.NetworkPermission");
60+
}
61+
62+
private String getPolicyUrl() {
63+
return getClass().getResource("security-manager-test.policy").toExternalForm();
64+
}
65+
66+
@Test
67+
void buildWithDefaults_whenUnrelatedSecurityExceptionThrown_shouldNotThrow() {
68+
System.setSecurityManager(new SecurityManager() {
69+
@Override
70+
public void checkPermission(Permission perm) {
71+
if ("jdk.net.NetworkPermission".equals(perm.getClass().getName())) {
72+
throw new SecurityException("access denied: some.unrelated.permission");
73+
}
74+
}
75+
});
76+
77+
try (LogCaptor logCaptor = LogCaptor.create(Level.DEBUG)) {
78+
assertThatNoException().isThrownBy(() -> {
79+
Apache5HttpClient.builder().build().close();
80+
});
81+
assertThat(logCaptor.loggedEvents()).anySatisfy(logEvent -> {
82+
assertThat(logEvent.getLevel()).isEqualTo(Level.DEBUG);
83+
assertThat(logEvent.getMessage().getFormattedMessage())
84+
.contains("SecurityManager denied a non-TCP socket option permission");
85+
});
86+
}
87+
}
88+
89+
@ParameterizedTest
90+
@MethodSource("partiallyGrantedPermissions")
91+
void buildWithDefaults_whenNotAllPermissionsGranted_shouldThrowIllegalStateException(Set<String> grantedPermissions) {
92+
System.setSecurityManager(new GrantOnlyNetworkPermissionSecurityManager(grantedPermissions));
93+
94+
assertThatThrownBy(() -> Apache5HttpClient.builder().build())
95+
.isInstanceOf(IllegalStateException.class)
96+
.hasMessageContaining("jdk.net.NetworkPermission");
97+
}
98+
99+
@Test
100+
void buildWithDefaults_whenAllPermissionsGranted_shouldSucceed() {
101+
Set<String> allGranted = new HashSet<>(Arrays.asList(
102+
"setOption.TCP_KEEPIDLE", "setOption.TCP_KEEPINTERVAL", "setOption.TCP_KEEPCOUNT"));
103+
System.setSecurityManager(new GrantOnlyNetworkPermissionSecurityManager(allGranted));
104+
assertThatNoException().isThrownBy(() -> {
105+
Apache5HttpClient.builder().build().close();
106+
});
107+
}
108+
109+
@Test
110+
void buildWithDefaults_whenNoSecurityManager_shouldSucceed() {
111+
assertThatNoException().isThrownBy(() -> {
112+
Apache5HttpClient.builder().build().close();
113+
});
114+
}
115+
116+
static Stream<Arguments> partiallyGrantedPermissions() {
117+
return Stream.of(
118+
// 0 out of 3 granted
119+
Arguments.of(new HashSet<>()),
120+
// 1 out of 3 granted
121+
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPIDLE"))),
122+
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPINTERVAL"))),
123+
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPCOUNT"))),
124+
// 2 out of 3 granted
125+
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPIDLE", "setOption.TCP_KEEPINTERVAL"))),
126+
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPIDLE", "setOption.TCP_KEEPCOUNT"))),
127+
Arguments.of(new HashSet<>(Arrays.asList("setOption.TCP_KEEPINTERVAL", "setOption.TCP_KEEPCOUNT")))
128+
);
129+
}
130+
131+
/**
132+
* SecurityManager that only grants specific jdk.net.NetworkPermission entries and denies the rest.
133+
*/
134+
private static class GrantOnlyNetworkPermissionSecurityManager extends SecurityManager {
135+
private final Set<String> grantedPermissions;
136+
137+
GrantOnlyNetworkPermissionSecurityManager(Set<String> grantedPermissions) {
138+
this.grantedPermissions = grantedPermissions;
139+
}
140+
141+
@Override
142+
public void checkPermission(Permission perm) {
143+
if ("jdk.net.NetworkPermission".equals(perm.getClass().getName())
144+
&& !grantedPermissions.contains(perm.getName())) {
145+
throw new SecurityException("Denied: " + perm.getName());
146+
}
147+
}
148+
}
149+
150+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
16+
package software.amazon.awssdk.http.apache5;
17+
18+
import org.junit.jupiter.api.condition.EnabledForJreRange;
19+
import org.junit.jupiter.api.condition.JRE;
20+
import software.amazon.awssdk.http.SdkHttpClient;
21+
import software.amazon.awssdk.http.SdkHttpClientSecurityManagerTestSuite;
22+
23+
@EnabledForJreRange(max = JRE.JAVA_17)
24+
class Apache5SecurityManagerHttpCallTest extends SdkHttpClientSecurityManagerTestSuite {
25+
26+
@Override
27+
protected SdkHttpClient createHttpClient() {
28+
return Apache5HttpClient.builder().build();
29+
}
30+
31+
@Override
32+
protected String getPolicyFileUrl() {
33+
return getClass().getResource("security-manager-test-with-http-call.policy").toExternalForm();
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
grant {
2+
permission java.util.PropertyPermission "*", "read,write";
3+
permission java.lang.RuntimePermission "modifyThread";
4+
permission java.lang.RuntimePermission "setContextClassLoader";
5+
permission java.lang.RuntimePermission "setSecurityManager";
6+
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
7+
permission java.net.SocketPermission "*", "connect,accept";
8+
9+
// Required by Apache HC5 for TCP socket options (not needed by Apache HC4)
10+
permission jdk.net.NetworkPermission "setOption.TCP_KEEPIDLE";
11+
permission jdk.net.NetworkPermission "setOption.TCP_KEEPINTERVAL";
12+
permission jdk.net.NetworkPermission "setOption.TCP_KEEPCOUNT";
13+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
grant {
2+
permission java.util.PropertyPermission "*", "read,write";
3+
permission java.io.FilePermission "<<ALL FILES>>", "read,write";
4+
permission java.lang.RuntimePermission "getenv.*";
5+
permission "java.lang.RuntimePermission" "accessDeclaredMembers";
6+
permission "javax.net.ssl.SSLPermission" "setDefaultSSLContext";
7+
permission "java.net.SocketPermission" "*", "connect,resolve";
8+
9+
// Needed for test to remove the security manager
10+
permission java.lang.RuntimePermission "setSecurityManager";
11+
12+
// jdk.net.NetworkPermission for setOption.TCP_KEEPIDLE, setOption.TCP_KEEPINTERVAL,
13+
// setOption.TCP_KEEPCOUNT is explicitly NOT granted to test that Apache5HttpClient
14+
// fails fast when these permissions are missing.
15+
};

0 commit comments

Comments
 (0)