task-65: transient payload 读取流式解压 + 解压后字节上限
- 新增 aiimage.transient-storage.max-decompressed-payload-bytes 配置 (默认 100MB,即 2x 存储上限),覆盖 gzip 二进制与 gzip64 兼容两条路径 - decodeGzipStream 按 8KB 缓冲流式解压,累计输出超过上限立即中止并报错, 防止压缩炸弹(zip bomb)在读取时无界膨胀内存 - 超限错误保留可识别消息(exceeds configured limit),由 resolvePayload 统一包装
This commit is contained in:
@@ -37,6 +37,11 @@ public class TransientStorageProperties {
|
||||
private long warnPayloadBytes = 5L * 1024 * 1024;
|
||||
private long maxPayloadBytes = 50L * 1024 * 1024;
|
||||
private long maxStoredPayloadBytes = 50L * 1024 * 1024;
|
||||
/**
|
||||
* 读取端解压后字节上限:防止压缩炸弹(zip bomb)在流式解压时无界膨胀内存。
|
||||
* 默认 2x 存储上限,覆盖 gzip 二进制路径与 gzip64 兼容路径。
|
||||
*/
|
||||
private long maxDecompressedPayloadBytes = 100L * 1024 * 1024;
|
||||
private boolean fallbackToLocalOnOversize = true;
|
||||
private boolean deleteRetryEnabled = true;
|
||||
private String deleteRetryCron = "0 */5 * * * *";
|
||||
|
||||
+35
-8
@@ -467,19 +467,15 @@ public class TransientPayloadStorageService {
|
||||
return "";
|
||||
}
|
||||
if (isGzip(storedContent)) {
|
||||
try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(storedContent))) {
|
||||
return new String(gzip.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to decode transient payload", ex);
|
||||
}
|
||||
return decodeGzipStream(storedContent);
|
||||
}
|
||||
String legacyText = new String(storedContent, StandardCharsets.UTF_8);
|
||||
if (legacyText.startsWith("gzip64:")) {
|
||||
try {
|
||||
byte[] compressed = Base64.getDecoder().decode(legacyText.substring("gzip64:".length()));
|
||||
try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(compressed))) {
|
||||
return new String(gzip.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
return decodeGzipStream(compressed);
|
||||
} catch (IllegalStateException ex) {
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to decode transient payload", ex);
|
||||
}
|
||||
@@ -488,6 +484,37 @@ public class TransientPayloadStorageService {
|
||||
return legacyText;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式解压 gzip 字节流,解压输出累计超过 maxDecompressedPayloadBytes 立即中止,
|
||||
* 防止压缩炸弹在内存中无界膨胀。上限未配置(<=0)时不限流。
|
||||
*/
|
||||
private String decodeGzipStream(byte[] compressed) {
|
||||
long maxBytes = properties.getMaxDecompressedPayloadBytes();
|
||||
try {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try (GZIPInputStream gzip = new GZIPInputStream(new ByteArrayInputStream(compressed))) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = gzip.read(buffer)) >= 0) {
|
||||
if (read == 0) {
|
||||
continue;
|
||||
}
|
||||
if (isPositiveLimit(maxBytes) && (long) out.size() + read > maxBytes) {
|
||||
throw new IllegalStateException(
|
||||
"transient payload decompressed size exceeds configured limit "
|
||||
+ maxBytes + " bytes");
|
||||
}
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
return out.toString(StandardCharsets.UTF_8);
|
||||
} catch (IllegalStateException ex) {
|
||||
throw ex;
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException("failed to decode transient payload", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isGzip(byte[] content) {
|
||||
return content.length >= 2
|
||||
&& (content[0] & 0xFF) == 0x1F
|
||||
|
||||
Reference in New Issue
Block a user