Skip to content

Commit 1241575

Browse files
authored
Fix: add size limit when reading data from a zip file entry (#1673)
* fix: ensure that only a given maximum number of bytes are read when reading a zip archive entry, cleanups * add check for negative file sizes and read at most the number of declared size in the zip entry * include crafted test zip with wrong file sizes, improve tests and error messages
1 parent 3cf4deb commit 1241575

7 files changed

Lines changed: 65 additions & 16 deletions

File tree

server/src/main/java/org/eclipse/openvsx/ExtensionService.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import jakarta.transaction.Transactional;
1515
import jakarta.transaction.Transactional.TxType;
1616
import org.apache.commons.io.FileUtils;
17+
import org.apache.commons.io.IOUtils;
1718
import org.apache.commons.lang3.StringUtils;
1819
import org.eclipse.openvsx.admin.RemoveFileJobRequest;
1920
import org.eclipse.openvsx.cache.CacheService;
@@ -187,9 +188,7 @@ private TempFile createExtensionFile(InputStream content) {
187188
}
188189

189190
if (size > maxContentSize) {
190-
try {
191-
extensionFile.close();
192-
} catch (IOException _) {}
191+
IOUtils.closeQuietly(extensionFile);
193192
var maxSize = FileUtils.byteCountToDisplaySize(maxContentSize);
194193
throw new ErrorResultException("The extension package exceeds the size limit of " + maxSize + ".", HttpStatus.PAYLOAD_TOO_LARGE);
195194
}

server/src/main/java/org/eclipse/openvsx/util/ArchiveUtil.java

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
********************************************************************************/
1010
package org.eclipse.openvsx.util;
1111

12+
import jakarta.annotation.Nullable;
13+
import org.apache.commons.io.FileUtils;
14+
import org.apache.commons.io.IOUtils;
15+
1216
import java.io.IOException;
1317
import java.nio.file.Files;
1418
import java.nio.file.InvalidPathException;
@@ -25,31 +29,52 @@ public final class ArchiveUtil {
2529

2630
private ArchiveUtil() {}
2731

28-
public static ZipEntry getEntryIgnoreCase(ZipFile archive, String entryName) {
32+
public static @Nullable ZipEntry getEntryIgnoreCase(ZipFile archive, String entryName) {
2933
return archive.stream()
3034
.filter(entry -> entry.getName().equalsIgnoreCase(entryName))
3135
.findAny()
3236
.orElse(null);
3337
}
3438

3539
public static TempFile readEntry(ZipFile archive, String entryName) throws IOException {
40+
return readEntry(archive, entryName, MAX_ENTRY_SIZE);
41+
}
42+
43+
static TempFile readEntry(ZipFile archive, String entryName, long maxEntrySize) throws IOException {
3644
var entry = archive.getEntry(entryName);
37-
if (entry == null)
38-
return null;
39-
return readEntry(archive, entry);
45+
return entry != null ? readEntry(archive, entry, maxEntrySize) : null;
4046
}
4147

4248
public static TempFile readEntry(ZipFile archive, ZipEntry entry) throws IOException {
49+
return readEntry(archive, entry, MAX_ENTRY_SIZE);
50+
}
51+
52+
static TempFile readEntry(ZipFile archive, ZipEntry entry, long maxEntrySize) throws IOException {
4353
var fileNameIndex = entry.getName().lastIndexOf('/');
4454
var fileName = fileNameIndex == -1 ? entry.getName() : entry.getName().substring(fileNameIndex + 1);
4555
var suffixIndex = fileName.lastIndexOf('.');
4656
var suffix = suffixIndex == -1 ? null : fileName.substring(suffixIndex);
4757
var prefix = suffixIndex == -1 ? fileName : fileName.substring(0, suffixIndex);
4858
var file = new TempFile(prefix, suffix);
4959
try (var out = Files.newOutputStream(file.getPath())){
50-
if (entry.getSize() > MAX_ENTRY_SIZE)
51-
throw new ErrorResultException("The file " + entry.getName() + " exceeds the size limit of 32 MB.");
52-
archive.getInputStream(entry).transferTo(out);
60+
if (entry.getSize() < 0) {
61+
IOUtils.closeQuietly(file);
62+
throw new ErrorResultException("The file " + entry.getName() + " has a negative size.");
63+
}
64+
65+
if (entry.getSize() > maxEntrySize) {
66+
IOUtils.closeQuietly(file);
67+
var maxSize = FileUtils.byteCountToDisplaySize(maxEntrySize);
68+
throw new ErrorResultException("The file " + entry.getName() + " exceeds the size limit of " + maxSize + ".");
69+
}
70+
71+
// Read at most the number of bytes as declared by the entry
72+
try (var is = new SizeLimitInputStream(archive.getInputStream(entry), entry.getSize())) {
73+
is.transferTo(out);
74+
} catch (IOException e) {
75+
IOUtils.closeQuietly(file);
76+
throw new ErrorResultException("Failed to read " + entry.getName() + ": " + e.getMessage());
77+
}
5378
}
5479
return file;
5580
}
@@ -92,5 +117,4 @@ public static boolean isSafePath(String filePath) {
92117
return false;
93118
}
94119
}
95-
96120
}

server/src/main/java/org/eclipse/openvsx/util/FileUtil.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*
88
* SPDX-License-Identifier: EPL-2.0
99
* ****************************************************************************** */
10-
1110
package org.eclipse.openvsx.util;
1211

1312
import java.nio.file.Files;
@@ -31,7 +30,7 @@ protected boolean removeEldestEntry(Map.Entry eldest){
3130
});
3231
}
3332

34-
private FileUtil(){}
33+
private FileUtil() {}
3534

3635
/***
3736
* Write to file synchronously, if it doesn't already exist.

server/src/main/java/org/eclipse/openvsx/util/SizeLimitInputStream.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
********************************************************************************/
1313
package org.eclipse.openvsx.util;
1414

15+
import org.apache.commons.io.FileUtils;
16+
1517
import java.io.FilterInputStream;
1618
import java.io.IOException;
1719
import java.io.InputStream;
@@ -51,7 +53,8 @@ public int read(byte[] b, int off, int len) throws IOException {
5153
private void checkLimit(long n) throws IOException {
5254
bytesRead += n;
5355
if (bytesRead > maxBytes) {
54-
throw new IOException("File size exceeds limit of " + maxBytes + " bytes");
56+
var maxSize = FileUtils.byteCountToDisplaySize(maxBytes);
57+
throw new IOException("File size exceeds limit of " + maxSize + ".");
5558
}
5659
}
5760
}

server/src/main/java/org/eclipse/openvsx/util/TempFile.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@
1212
import org.eclipse.openvsx.entities.FileResource;
1313
import org.eclipse.openvsx.entities.Namespace;
1414

15+
import java.io.Closeable;
1516
import java.io.IOException;
1617
import java.nio.file.Files;
1718
import java.nio.file.Path;
1819

19-
public class TempFile implements AutoCloseable {
20+
public class TempFile implements Closeable {
2021

2122
private final Path path;
2223
private FileResource resource;

server/src/test/java/org/eclipse/openvsx/util/ArchiveUtilTest.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
********************************************************************************/
1010
package org.eclipse.openvsx.util;
1111

12-
import static org.assertj.core.api.Assertions.assertThat;
12+
import static org.assertj.core.api.Assertions.*;
13+
import static org.assertj.core.api.InstanceOfAssertFactories.PATH;
1314

15+
import java.io.IOException;
1416
import java.nio.file.Files;
1517
import java.util.zip.ZipFile;
1618

@@ -21,7 +23,10 @@ class ArchiveUtilTest {
2123
@Test
2224
void testTodoTree() throws Exception {
2325
var packageUrl = getClass().getResource("todo-tree.zip");
26+
27+
assertThat(packageUrl).isNotNull();
2428
assertThat(packageUrl.getProtocol()).isEqualTo("file");
29+
2530
try (
2631
var archive = new ZipFile(packageUrl.getPath());
2732
var packageFile = ArchiveUtil.readEntry(archive, "extension/package.json");
@@ -34,4 +39,22 @@ void testTodoTree() throws Exception {
3439
}
3540
}
3641

42+
@Test
43+
void testExceedMaxEntrySize() throws IOException {
44+
// an artificially crafted zip file with a file whose size is set lower as its actual content
45+
var packageUrl = getClass().getResource("wrong-size.zip");
46+
47+
assertThat(packageUrl).isNotNull();
48+
assertThat(packageUrl.getProtocol()).isEqualTo("file");
49+
50+
try (var archive = new ZipFile(packageUrl.getPath())) {
51+
assertThatThrownBy(() -> ArchiveUtil.readEntry(archive, "extension/README.md", 8192))
52+
.isExactlyInstanceOf(ErrorResultException.class)
53+
.hasMessage("The file extension/README.md exceeds the size limit of 8 KB.");
54+
55+
assertThatThrownBy(() -> ArchiveUtil.readEntry(archive, "extension/package.json", 8192))
56+
.isExactlyInstanceOf(ErrorResultException.class)
57+
.hasMessageContaining("Failed to read extension/package.json: File size exceeds limit of 0 bytes.");
58+
}
59+
}
3760
}
Binary file not shown.

0 commit comments

Comments
 (0)