diff --git a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java index f5a3d40635..2607b4eba9 100644 --- a/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java +++ b/application/src/main/java/org/thingsboard/server/service/solutions/DefaultSolutionService.java @@ -144,6 +144,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -181,6 +182,15 @@ public class DefaultSolutionService implements SolutionService { @Value("${ui.solution_templates.docs_base_url:https://thingsboard.io/docs}") private String docsBaseUrl; + @Value("${iot-hub.max-uncompressed-archive-bytes:209715200}") + private long maxUncompressedArchiveBytes; + + @Value("${iot-hub.max-uncompressed-entry-bytes:52428800}") + private long maxUncompressedEntryBytes; + + @Value("${iot-hub.max-archive-entry-count:10000}") + private int maxArchiveEntryCount; + private final RuleChainService ruleChainService; private final TbRuleChainService tbRuleChainService; private final DeviceProfileService deviceProfileService; @@ -1555,10 +1565,18 @@ public class DefaultSolutionService implements SolutionService { return Collections.emptyList(); } - private static void extractZip(byte[] zipData, Path destDir) throws IOException { + private static final int EXTRACT_BUFFER_SIZE = 8 * 1024; + + private void extractZip(byte[] zipData, Path destDir) throws IOException { + long totalBytes = 0; + int entryCount = 0; + byte[] buf = new byte[EXTRACT_BUFFER_SIZE]; try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipData))) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { + if (++entryCount > maxArchiveEntryCount) { + throw new IOException("Solution template archive exceeds max entry count: " + maxArchiveEntryCount); + } Path entryPath = destDir.resolve(entry.getName()).normalize(); if (!entryPath.startsWith(destDir)) { throw new IOException("ZIP entry outside of target directory: " + entry.getName()); @@ -1567,7 +1585,21 @@ public class DefaultSolutionService implements SolutionService { Files.createDirectories(entryPath); } else { Files.createDirectories(entryPath.getParent()); - Files.write(entryPath, zis.readAllBytes()); + try (OutputStream out = Files.newOutputStream(entryPath)) { + long entryBytes = 0; + int n; + while ((n = zis.read(buf)) > 0) { + entryBytes += n; + totalBytes += n; + if (entryBytes > maxUncompressedEntryBytes) { + throw new IOException("Solution template entry exceeds max uncompressed size: " + entry.getName()); + } + if (totalBytes > maxUncompressedArchiveBytes) { + throw new IOException("Solution template archive exceeds max uncompressed size: " + maxUncompressedArchiveBytes + " bytes"); + } + out.write(buf, 0, n); + } + } } zis.closeEntry(); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index a0ae65bfe8..d6ca6ccce8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -2199,3 +2199,6 @@ iot-hub: connect-timeout-sec: "${IOT_HUB_CONNECT_TIMEOUT_SEC:5}" # timeout in seconds to connect to IoT Hub server. read-timeout-sec: "${IOT_HUB_READ_TIMEOUT_SEC:10}" # timeout in seconds to read from IoT Hub server. max-file-data-size-bytes: "${IOT_HUB_MAX_FILE_DATA_SIZE_BYTES:104857600}" # maximum size in bytes of a file-data payload fetched from IoT Hub. Requests advertising a larger Content-Length, or streams that exceed this size, are rejected to avoid out-of-memory issues on the platform side. + max-uncompressed-archive-bytes: "${IOT_HUB_MAX_UNCOMPRESSED_ARCHIVE_BYTES:209715200}" # maximum cumulative uncompressed size in bytes for a solution template archive. Extraction aborts past this threshold to mitigate zip-bomb attacks. + max-uncompressed-entry-bytes: "${IOT_HUB_MAX_UNCOMPRESSED_ENTRY_BYTES:52428800}" # maximum uncompressed size in bytes for any single entry inside a solution template archive. + max-archive-entry-count: "${IOT_HUB_MAX_ARCHIVE_ENTRY_COUNT:10000}" # maximum number of entries allowed inside a solution template archive.