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 (
|
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{}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user