Skip to content

Commit 1ff2c13

Browse files
joachimdraegerTim-Brooks
authored andcommitted
Avoid SecurityException in repository-S3 on DefaultS3OutputStream.flush() (#25254)
Moved SocketAccess.doPrivileged up the stack to DefaultS3OutputStream in repository-S3 plugin to avoid SecurityException by Streams.copy(). A plugin is only allowed to use its own jars when performing privileged operations. The S3 client might open a new Socket on close(). #25192
1 parent 0e8d758 commit 1ff2c13

File tree

4 files changed

+84
-2
lines changed

4 files changed

+84
-2
lines changed

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/DefaultS3OutputStream.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ class DefaultS3OutputStream extends S3OutputStream {
7878

7979
@Override
8080
public void flush(byte[] bytes, int off, int len, boolean closing) throws IOException {
81+
SocketAccess.doPrivilegedIOException(() -> {
82+
flushPrivileged(bytes, off, len, closing);
83+
return null;
84+
});
85+
}
86+
87+
private void flushPrivileged(byte[] bytes, int off, int len, boolean closing) throws IOException {
8188
if (len > MULTIPART_MAX_SIZE.getBytes()) {
8289
throw new IOException("Unable to upload files larger than " + MULTIPART_MAX_SIZE + " to Amazon S3");
8390
}

plugins/repository-s3/src/main/java/org/elasticsearch/repositories/s3/S3BlobContainer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public void writeBlob(String blobName, InputStream inputStream, long blobSize) t
9292
throw new FileAlreadyExistsException("blob [" + blobName + "] already exists, cannot overwrite");
9393
}
9494
try (OutputStream stream = createOutput(blobName)) {
95-
SocketAccess.doPrivilegedIOException(() -> Streams.copy(inputStream, stream));
95+
Streams.copy(inputStream, stream);
9696
}
9797
}
9898

plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/MockAmazonS3.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,49 @@
4040

4141
import java.io.IOException;
4242
import java.io.InputStream;
43+
import java.io.UncheckedIOException;
44+
import java.net.InetAddress;
45+
import java.net.Socket;
4346
import java.security.DigestInputStream;
4447
import java.util.ArrayList;
4548
import java.util.List;
4649
import java.util.Map;
4750
import java.util.concurrent.ConcurrentHashMap;
4851

52+
import static org.junit.Assert.assertTrue;
53+
4954
class MockAmazonS3 extends AbstractAmazonS3 {
5055

56+
private final int mockSocketPort;
57+
5158
private Map<String, InputStream> blobs = new ConcurrentHashMap<>();
5259

5360
// in ESBlobStoreContainerTestCase.java, the maximum
5461
// length of the input data is 100 bytes
5562
private byte[] byteCounter = new byte[100];
5663

64+
65+
MockAmazonS3(int mockSocketPort) {
66+
this.mockSocketPort = mockSocketPort;
67+
}
68+
69+
// Simulate a socket connection to check that SocketAccess.doPrivileged() is used correctly.
70+
// Any method of AmazonS3 might potentially open a socket to the S3 service. Firstly, a call
71+
// to any method of AmazonS3 has to be wrapped by SocketAccess.doPrivileged().
72+
// Secondly, each method on the stack from doPrivileged to opening the socket has to be
73+
// located in a jar that is provided by the plugin.
74+
// Thirdly, a SocketPermission has to be configured in plugin-security.policy.
75+
// By opening a socket in each method of MockAmazonS3 it is ensured that in production AmazonS3
76+
// is able to to open a socket to the S3 Service without causing a SecurityException
77+
private void simulateS3SocketConnection() {
78+
try (Socket socket = new Socket(InetAddress.getByName("127.0.0.1"), mockSocketPort)) {
79+
assertTrue(socket.isConnected()); // NOOP to keep static analysis happy
80+
} catch (IOException e) {
81+
throw new UncheckedIOException(e);
82+
}
83+
}
84+
85+
5786
@Override
5887
public boolean doesBucketExist(String bucket) {
5988
return true;
@@ -63,6 +92,7 @@ public boolean doesBucketExist(String bucket) {
6392
public ObjectMetadata getObjectMetadata(
6493
GetObjectMetadataRequest getObjectMetadataRequest)
6594
throws AmazonClientException, AmazonServiceException {
95+
simulateS3SocketConnection();
6696
String blobName = getObjectMetadataRequest.getKey();
6797

6898
if (!blobs.containsKey(blobName)) {
@@ -75,6 +105,7 @@ public ObjectMetadata getObjectMetadata(
75105
@Override
76106
public PutObjectResult putObject(PutObjectRequest putObjectRequest)
77107
throws AmazonClientException, AmazonServiceException {
108+
simulateS3SocketConnection();
78109
String blobName = putObjectRequest.getKey();
79110

80111
if (blobs.containsKey(blobName)) {
@@ -88,6 +119,7 @@ public PutObjectResult putObject(PutObjectRequest putObjectRequest)
88119
@Override
89120
public S3Object getObject(GetObjectRequest getObjectRequest)
90121
throws AmazonClientException, AmazonServiceException {
122+
simulateS3SocketConnection();
91123
// in ESBlobStoreContainerTestCase.java, the prefix is empty,
92124
// so the key and blobName are equivalent to each other
93125
String blobName = getObjectRequest.getKey();
@@ -107,6 +139,7 @@ public S3Object getObject(GetObjectRequest getObjectRequest)
107139
@Override
108140
public ObjectListing listObjects(ListObjectsRequest listObjectsRequest)
109141
throws AmazonClientException, AmazonServiceException {
142+
simulateS3SocketConnection();
110143
MockObjectListing list = new MockObjectListing();
111144
list.setTruncated(false);
112145

@@ -140,6 +173,7 @@ public ObjectListing listObjects(ListObjectsRequest listObjectsRequest)
140173
@Override
141174
public CopyObjectResult copyObject(CopyObjectRequest copyObjectRequest)
142175
throws AmazonClientException, AmazonServiceException {
176+
simulateS3SocketConnection();
143177
String sourceBlobName = copyObjectRequest.getSourceKey();
144178
String targetBlobName = copyObjectRequest.getDestinationKey();
145179

@@ -160,6 +194,7 @@ public CopyObjectResult copyObject(CopyObjectRequest copyObjectRequest)
160194
@Override
161195
public void deleteObject(DeleteObjectRequest deleteObjectRequest)
162196
throws AmazonClientException, AmazonServiceException {
197+
simulateS3SocketConnection();
163198
String blobName = deleteObjectRequest.getKey();
164199

165200
if (!blobs.containsKey(blobName)) {

plugins/repository-s3/src/test/java/org/elasticsearch/repositories/s3/S3BlobStoreContainerTests.java

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,61 @@
1919

2020
package org.elasticsearch.repositories.s3;
2121

22+
import org.apache.logging.log4j.Level;
23+
import org.apache.logging.log4j.Logger;
2224
import org.elasticsearch.common.blobstore.BlobStore;
25+
import org.elasticsearch.common.logging.Loggers;
2326
import org.elasticsearch.common.settings.Settings;
2427
import org.elasticsearch.common.unit.ByteSizeUnit;
2528
import org.elasticsearch.common.unit.ByteSizeValue;
29+
import org.elasticsearch.mocksocket.MockServerSocket;
2630
import org.elasticsearch.repositories.ESBlobStoreContainerTestCase;
31+
import org.junit.AfterClass;
32+
import org.junit.BeforeClass;
2733

2834
import java.io.IOException;
35+
import java.net.InetAddress;
36+
import java.net.ServerSocket;
2937
import java.util.Locale;
3038

3139
public class S3BlobStoreContainerTests extends ESBlobStoreContainerTestCase {
40+
41+
private static final Logger logger = Loggers.getLogger(S3BlobStoreContainerTests.class);
42+
43+
private static ServerSocket mockS3ServerSocket;
44+
45+
private static Thread mockS3AcceptorThread;
46+
47+
// Opens a MockSocket to simulate connections to S3 checking that SocketPermissions are set up correctly.
48+
// See MockAmazonS3.simulateS3SocketConnection.
49+
@BeforeClass
50+
public static void openMockSocket() throws IOException {
51+
mockS3ServerSocket = new MockServerSocket(0, 50, InetAddress.getByName("127.0.0.1"));
52+
mockS3AcceptorThread = new Thread(() -> {
53+
while (!mockS3ServerSocket.isClosed()) {
54+
try {
55+
// Accept connections from MockAmazonS3.
56+
mockS3ServerSocket.accept();
57+
} catch (IOException e) {
58+
}
59+
}
60+
});
61+
mockS3AcceptorThread.start();
62+
}
63+
3264
protected BlobStore newBlobStore() throws IOException {
33-
MockAmazonS3 client = new MockAmazonS3();
65+
MockAmazonS3 client = new MockAmazonS3(mockS3ServerSocket.getLocalPort());
3466
String bucket = randomAlphaOfLength(randomIntBetween(1, 10)).toLowerCase(Locale.ROOT);
3567

3668
return new S3BlobStore(Settings.EMPTY, client, bucket, false,
3769
new ByteSizeValue(10, ByteSizeUnit.MB), "public-read-write", "standard");
3870
}
71+
72+
@AfterClass
73+
public static void closeMockSocket() throws IOException, InterruptedException {
74+
mockS3ServerSocket.close();
75+
mockS3AcceptorThread.join();
76+
mockS3AcceptorThread = null;
77+
mockS3ServerSocket = null;
78+
}
3979
}

0 commit comments

Comments
 (0)