From 95408e68d4fb00a0577987dd88152f3d68d9dd5f Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 8 Jan 2026 08:50:18 -0800 Subject: [PATCH] Implement max input dimensions and path traversal validation - Reject input images exceeding MaxInputDimension (8192px) to prevent DoS - Detect path traversal: ../, encoded variants, backslashes, null bytes --- internal/imgcache/processor.go | 5 +++ internal/imgcache/urlparser.go | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/internal/imgcache/processor.go b/internal/imgcache/processor.go index 8d26a0a..cf629ad 100644 --- a/internal/imgcache/processor.go +++ b/internal/imgcache/processor.go @@ -53,6 +53,11 @@ func (p *ImageProcessor) Process( origWidth := bounds.Dx() origHeight := bounds.Dy() + // Validate input dimensions to prevent DoS via decompression bombs + if origWidth > MaxInputDimension || origHeight > MaxInputDimension { + return nil, ErrInputTooLarge + } + // Determine target dimensions targetWidth := req.Size.Width targetHeight := req.Size.Height diff --git a/internal/imgcache/urlparser.go b/internal/imgcache/urlparser.go index d911100..ec69d9c 100644 --- a/internal/imgcache/urlparser.go +++ b/internal/imgcache/urlparser.go @@ -3,6 +3,7 @@ package imgcache import ( "errors" "fmt" + "net/url" "regexp" "strconv" "strings" @@ -74,6 +75,11 @@ func ParseImageURL(urlPath string) (*ParsedURL, error) { // parseImageComponents parses //. structure. func parseImageComponents(remainder string) (*ParsedURL, error) { + // Check for path traversal before any other processing + if err := checkPathTraversal(remainder); err != nil { + return nil, err + } + // Find the last path segment which contains size.format lastSlash := strings.LastIndex(remainder, "/") if lastSlash == -1 { @@ -131,6 +137,64 @@ func parseImageComponents(remainder string) (*ParsedURL, error) { }, nil } +// checkPathTraversal detects path traversal attempts in a URL path. +// It checks for various attack vectors including: +// - Direct ../ sequences +// - URL-encoded variants (%2e%2e, %252e%252e) +// - Backslash variants (..\) +// - Null byte injection (%00) +func checkPathTraversal(path string) error { + // First, URL-decode the path to catch encoded attacks + // Decode multiple times to catch double-encoding + decoded := path + for range 3 { + newDecoded, err := url.PathUnescape(decoded) + if err != nil { + // Malformed encoding is suspicious + return ErrPathTraversal + } + + if newDecoded == decoded { + break + } + + decoded = newDecoded + } + + // Normalize backslashes to forward slashes + normalized := strings.ReplaceAll(decoded, "\\", "/") + + // Check for null bytes + if strings.Contains(normalized, "\x00") { + return ErrPathTraversal + } + + // Check for parent directory traversal + // Look for "/.." or "../" patterns + if strings.Contains(normalized, "/../") || + strings.Contains(normalized, "/..") || + strings.HasPrefix(normalized, "../") || + strings.HasSuffix(normalized, "/..") || + normalized == ".." { + return ErrPathTraversal + } + + // Also check for ".." as a path segment in the original path + // This catches cases where the path hasn't been normalized + segments := strings.Split(path, "/") + for _, seg := range segments { + // URL decode the segment + decodedSeg, _ := url.PathUnescape(seg) + decodedSeg = strings.ReplaceAll(decodedSeg, "\\", "/") + + if decodedSeg == ".." { + return ErrPathTraversal + } + } + + return nil +} + // parseSizeFormat parses strings like "800x600.webp" or "orig.png" func parseSizeFormat(s string) (Size, ImageFormat, error) { matches := sizeFormatRegex.FindStringSubmatch(s)