From 71d45297df1bab5ef3e3675c6b7501e7ed6d2256 Mon Sep 17 00:00:00 2001 From: sec-check Date: Thu, 27 Aug 2026 00:52:07 -0400 Subject: [PATCH] fix(dashboard): cap decompressed audit log reads to defuse gzip bombs readAuditLogFile gunzipped rotated audit backups with an unbounded io.ReadAll, so a crafted or corrupted .gz in /data could expand a few KB into gigabytes and exhaust dashboard memory. Wrap the reader in io.LimitReader capped at 64MB (>10x the 5MB lumberjack rotation size), so no legitimate file is truncated and a clipped trailing line is harmlessly skipped by the JSON-line parser. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: sec-check --- src/pkg/dashboard/audit.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pkg/dashboard/audit.go b/src/pkg/dashboard/audit.go index e6787a119..9b7af7171 100644 --- a/src/pkg/dashboard/audit.go +++ b/src/pkg/dashboard/audit.go @@ -250,6 +250,15 @@ func auditLogFiles(filePath string) []string { return files } +// maxAuditFileReadBytes caps how many decompressed bytes readAuditLogFile will +// load from a single audit file. Lumberjack rotates at auditMaxSizeMB (5MB), so +// a legitimate file — compressed or not — decompresses to roughly that size. +// Without a cap, a crafted or corrupted ".gz" in /data (a gzip bomb: a few KB +// expanding to many GB) would let io.ReadAll exhaust dashboard memory. 64MB is +// >10x the rotation size, so no legitimate file is ever truncated; a truncated +// trailing line simply fails json.Unmarshal and is skipped by the caller. +const maxAuditFileReadBytes = 64 << 20 + func readAuditLogFile(path string) ([]byte, error) { f, err := os.Open(path) if err != nil { @@ -265,7 +274,7 @@ func readAuditLogFile(path string) ([]byte, error) { defer gz.Close() r = gz } - return io.ReadAll(r) + return io.ReadAll(io.LimitReader(r, maxAuditFileReadBytes)) } func (a *AuditLog) Recent(n int) []AuditEntry {