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
This commit is contained in:
2026-01-08 08:50:18 -08:00
parent c964feac7e
commit 95408e68d4
2 changed files with 69 additions and 0 deletions

View File

@@ -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

View File

@@ -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 <host>/<path>/<size>.<format> 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)