Skip to content

Commit d862b9b

Browse files
committed
Fix XSS sink through HttpRange header
1 parent 4079a75 commit d862b9b

File tree

1 file changed

+46
-40
lines changed

1 file changed

+46
-40
lines changed

server/src/main/java/com/adobe/testing/s3mock/ObjectController.java

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
import static com.adobe.testing.s3mock.util.HeaderUtil.storeHeadersFrom;
6969
import static com.adobe.testing.s3mock.util.HeaderUtil.userMetadataFrom;
7070
import static com.adobe.testing.s3mock.util.HeaderUtil.userMetadataHeadersFrom;
71+
import static org.springframework.http.HttpHeaders.ACCEPT_RANGES;
72+
import static org.springframework.http.HttpHeaders.CONTENT_RANGE;
7173
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
7274
import static org.springframework.http.HttpHeaders.IF_MATCH;
7375
import static org.springframework.http.HttpHeaders.IF_MODIFIED_SINCE;
@@ -288,7 +290,7 @@ public ResponseEntity<Void> headObject(
288290

289291
return ResponseEntity.ok()
290292
.eTag(s3ObjectMetadata.etag())
291-
.header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES)
293+
.header(ACCEPT_RANGES, RANGES_BYTES)
292294
.lastModified(s3ObjectMetadata.lastModified())
293295
.contentLength(Long.parseLong(s3ObjectMetadata.size()))
294296
.contentType(mediaTypeFrom(s3ObjectMetadata.contentType()))
@@ -408,7 +410,7 @@ public ResponseEntity<StreamingResponseBody> getObject(
408410
return ResponseEntity
409411
.ok()
410412
.eTag(s3ObjectMetadata.etag())
411-
.header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES)
413+
.header(ACCEPT_RANGES, RANGES_BYTES)
412414
.lastModified(s3ObjectMetadata.lastModified())
413415
.contentLength(Long.parseLong(s3ObjectMetadata.size()))
414416
.contentType(mediaTypeFrom(s3ObjectMetadata.contentType()))
@@ -973,48 +975,52 @@ public ResponseEntity<CopyObjectResult> copyObject(
973975
.body(new CopyObjectResult(copyS3ObjectMetadata));
974976
}
975977

976-
/**
977-
* Supports returning different ranges of an object.
978-
* E.g., if content has 100 bytes, the range request could be: bytes=10-100, 10--1 and 10-200
979-
* <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html">API Reference</a>
980-
*
981-
* @param range {@link String}
982-
* @param s3ObjectMetadata {@link S3ObjectMetadata}
983-
*/
984-
private ResponseEntity<StreamingResponseBody> getObjectWithRange(HttpRange range,
985-
S3ObjectMetadata s3ObjectMetadata) {
986-
var fileSize = s3ObjectMetadata.dataPath().toFile().length();
987-
var bytesToRead = Math.min(fileSize - 1, range.getRangeEnd(fileSize))
988-
- range.getRangeStart(fileSize) + 1;
989-
990-
if (bytesToRead < 0 || fileSize < range.getRangeStart(fileSize)) {
991-
return ResponseEntity.status(REQUESTED_RANGE_NOT_SATISFIABLE.value()).build();
978+
private ResponseEntity<StreamingResponseBody> getObjectWithRange(
979+
HttpRange range,
980+
S3ObjectMetadata s3ObjectMetadata
981+
) {
982+
final long fileSize = s3ObjectMetadata.dataPath().toFile().length();
983+
final long startInclusive = range.getRangeStart(fileSize);
984+
final long endInclusive = Math.min(fileSize - 1, range.getRangeEnd(fileSize));
985+
final long contentLength = endInclusive - startInclusive + 1;
986+
987+
if (contentLength < 0 || fileSize <= startInclusive) {
988+
return ResponseEntity.status(REQUESTED_RANGE_NOT_SATISFIABLE).build();
989+
}
990+
991+
return ResponseEntity
992+
.status(PARTIAL_CONTENT)
993+
.headers(headers -> applyS3MetadataHeaders(headers, s3ObjectMetadata))
994+
.header(ACCEPT_RANGES, RANGES_BYTES)
995+
.header(CONTENT_RANGE, String.format("bytes %d-%d/%d", startInclusive, endInclusive, fileSize))
996+
.eTag(s3ObjectMetadata.etag())
997+
.contentType(mediaTypeFrom(s3ObjectMetadata.contentType()))
998+
.lastModified(s3ObjectMetadata.lastModified())
999+
.contentLength(contentLength)
1000+
.body(outputStream ->
1001+
extractBytesToOutputStream(startInclusive, s3ObjectMetadata, outputStream, contentLength)
1002+
);
9921003
}
9931004

994-
return ResponseEntity
995-
.status(PARTIAL_CONTENT.value())
996-
.headers(headers -> headers.setAll(userMetadataHeadersFrom(s3ObjectMetadata)))
997-
.headers(headers -> headers.setAll(s3ObjectMetadata.storeHeaders()))
998-
.headers(headers -> headers.setAll(s3ObjectMetadata.encryptionHeaders()))
999-
.header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES)
1000-
.header(HttpHeaders.CONTENT_RANGE,
1001-
String.format("bytes %s-%s/%s",
1002-
range.getRangeStart(fileSize), bytesToRead + range.getRangeStart(fileSize) - 1,
1003-
s3ObjectMetadata.size()))
1004-
.eTag(s3ObjectMetadata.etag())
1005-
.contentType(mediaTypeFrom(s3ObjectMetadata.contentType()))
1006-
.lastModified(s3ObjectMetadata.lastModified())
1007-
.contentLength(bytesToRead)
1008-
.body(outputStream ->
1009-
extractBytesToOutputStream(range, s3ObjectMetadata, outputStream, fileSize, bytesToRead)
1010-
);
1011-
}
1005+
private void applyS3MetadataHeaders(HttpHeaders headers, S3ObjectMetadata metadata) {
1006+
headers.setAll(userMetadataHeadersFrom(metadata));
1007+
if (metadata.storeHeaders() != null) {
1008+
headers.setAll(metadata.storeHeaders());
1009+
}
1010+
if (metadata.encryptionHeaders() != null) {
1011+
headers.setAll(metadata.encryptionHeaders());
1012+
}
1013+
}
10121014

1013-
private static void extractBytesToOutputStream(HttpRange range, S3ObjectMetadata s3ObjectMetadata,
1014-
OutputStream outputStream, long fileSize, long bytesToRead) throws IOException {
1015+
private static void extractBytesToOutputStream(
1016+
long startOffset,
1017+
S3ObjectMetadata s3ObjectMetadata,
1018+
OutputStream outputStream,
1019+
long bytesToRead
1020+
) throws IOException {
10151021
try (var fis = Files.newInputStream(s3ObjectMetadata.dataPath())) {
1016-
var skip = fis.skip(range.getRangeStart(fileSize));
1017-
if (skip == range.getRangeStart(fileSize)) {
1022+
var skipped = fis.skip(startOffset);
1023+
if (skipped == startOffset) {
10181024
try (var bis = BoundedInputStream
10191025
.builder()
10201026
.setInputStream(fis)

0 commit comments

Comments
 (0)