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:
@@ -3,6 +3,7 @@ package imgcache
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/gif"
|
||||
@@ -14,6 +15,13 @@ import (
|
||||
"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.
|
||||
type ImageProcessor struct{}
|
||||
|
||||
|
||||
@@ -240,3 +240,74 @@ func TestImageProcessor_SupportedFormats(t *testing.T) {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ var (
|
||||
ErrInvalidSize = errors.New("invalid size format")
|
||||
ErrInvalidFormat = errors.New("invalid or unsupported format")
|
||||
ErrDimensionTooLarge = errors.New("dimension exceeds maximum")
|
||||
ErrPathTraversal = errors.New("path traversal detected")
|
||||
)
|
||||
|
||||
// MaxDimension is the maximum allowed width or height.
|
||||
|
||||
@@ -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).
|
||||
func errorIs(err, target error) bool {
|
||||
if err == target {
|
||||
|
||||
Reference in New Issue
Block a user