Add failing tests for input dimension and path traversal validation

Tests for:
- ErrInputTooLarge when input image exceeds MaxInputDimension
- ErrPathTraversal for ../, encoded traversal, backslashes, null bytes
This commit is contained in:
2026-01-08 08:48:11 -08:00
parent 857be30e82
commit c964feac7e
4 changed files with 168 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ package imgcache
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"image" "image"
"image/gif" "image/gif"
@@ -14,6 +15,13 @@ import (
"golang.org/x/image/webp" "golang.org/x/image/webp"
) )
// MaxInputDimension is the maximum allowed width or height for input images.
// Images larger than this are rejected to prevent DoS via decompression bombs.
const MaxInputDimension = 8192
// ErrInputTooLarge is returned when input image dimensions exceed MaxInputDimension.
var ErrInputTooLarge = errors.New("input image dimensions exceed maximum")
// ImageProcessor implements the Processor interface using pure Go libraries. // ImageProcessor implements the Processor interface using pure Go libraries.
type ImageProcessor struct{} type ImageProcessor struct{}

View File

@@ -240,3 +240,74 @@ func TestImageProcessor_SupportedFormats(t *testing.T) {
t.Error("SupportedOutputFormats() returned empty slice") t.Error("SupportedOutputFormats() returned empty slice")
} }
} }
func TestImageProcessor_RejectsOversizedInput(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// Create an image that exceeds MaxInputDimension (e.g., 10000x100)
// This should be rejected before processing to prevent DoS
input := createTestJPEG(t, 10000, 100)
req := &ImageRequest{
Size: Size{Width: 100, Height: 100},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
_, err := proc.Process(ctx, bytes.NewReader(input), req)
if err == nil {
t.Error("Process() should reject oversized input images")
}
if err != ErrInputTooLarge {
t.Errorf("Process() error = %v, want ErrInputTooLarge", err)
}
}
func TestImageProcessor_RejectsOversizedInputHeight(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// Create an image with oversized height
input := createTestJPEG(t, 100, 10000)
req := &ImageRequest{
Size: Size{Width: 100, Height: 100},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
_, err := proc.Process(ctx, bytes.NewReader(input), req)
if err == nil {
t.Error("Process() should reject oversized input images")
}
if err != ErrInputTooLarge {
t.Errorf("Process() error = %v, want ErrInputTooLarge", err)
}
}
func TestImageProcessor_AcceptsMaxDimensionInput(t *testing.T) {
proc := NewImageProcessor()
ctx := context.Background()
// Create an image at exactly MaxInputDimension - should be accepted
// Using smaller dimensions to keep test fast
input := createTestJPEG(t, MaxInputDimension, 100)
req := &ImageRequest{
Size: Size{Width: 100, Height: 100},
Format: FormatJPEG,
Quality: 85,
FitMode: FitCover,
}
result, err := proc.Process(ctx, bytes.NewReader(input), req)
if err != nil {
t.Fatalf("Process() should accept images at MaxInputDimension, got error: %v", err)
}
defer result.Content.Close()
}

View File

@@ -16,6 +16,7 @@ var (
ErrInvalidSize = errors.New("invalid size format") ErrInvalidSize = errors.New("invalid size format")
ErrInvalidFormat = errors.New("invalid or unsupported format") ErrInvalidFormat = errors.New("invalid or unsupported format")
ErrDimensionTooLarge = errors.New("dimension exceeds maximum") ErrDimensionTooLarge = errors.New("dimension exceeds maximum")
ErrPathTraversal = errors.New("path traversal detected")
) )
// MaxDimension is the maximum allowed width or height. // MaxDimension is the maximum allowed width or height.

View File

@@ -247,6 +247,94 @@ func TestParsedURL_ToImageRequest(t *testing.T) {
} }
} }
func TestParseImageURL_PathTraversal(t *testing.T) {
// All path traversal attempts should be rejected
tests := []struct {
name string
input string
}{
{
name: "simple parent directory",
input: "/v1/image/cdn.example.com/../etc/passwd/800x600.jpeg",
},
{
name: "double parent directory",
input: "/v1/image/cdn.example.com/../../etc/passwd/800x600.jpeg",
},
{
name: "parent in middle of path",
input: "/v1/image/cdn.example.com/photos/../../../etc/passwd/800x600.jpeg",
},
{
name: "encoded parent directory",
input: "/v1/image/cdn.example.com/photos/%2e%2e/secret/800x600.jpeg",
},
{
name: "double encoded parent",
input: "/v1/image/cdn.example.com/photos/%252e%252e/secret/800x600.jpeg",
},
{
name: "backslash traversal",
input: "/v1/image/cdn.example.com/photos/..\\secret/800x600.jpeg",
},
{
name: "mixed slashes",
input: "/v1/image/cdn.example.com/photos/../\\../secret/800x600.jpeg",
},
{
name: "null byte injection",
input: "/v1/image/cdn.example.com/photos/image.jpg%00.png/800x600.jpeg",
},
{
name: "parent at start of path",
input: "/v1/image/cdn.example.com/../800x600.jpeg",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseImageURL(tt.input)
if err == nil {
t.Error("ParseImageURL() should reject path traversal attempts")
}
if err != ErrPathTraversal {
t.Errorf("ParseImageURL() error = %v, want ErrPathTraversal", err)
}
})
}
}
func TestParseImagePath_PathTraversal(t *testing.T) {
// Test path traversal via ParseImagePath (chi wildcard)
tests := []struct {
name string
input string
}{
{
name: "parent directory in path",
input: "cdn.example.com/photos/../secret/800x600.jpeg",
},
{
name: "encoded traversal",
input: "cdn.example.com/photos/%2e%2e/secret/800x600.jpeg",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseImagePath(tt.input)
if err == nil {
t.Error("ParseImagePath() should reject path traversal attempts")
}
if err != ErrPathTraversal {
t.Errorf("ParseImagePath() error = %v, want ErrPathTraversal", err)
}
})
}
}
// errorIs checks if err matches target (handles wrapped errors). // errorIs checks if err matches target (handles wrapped errors).
func errorIs(err, target error) bool { func errorIs(err, target error) bool {
if err == target { if err == target {