fix(05): WR-04 restrict thumb mode and ext to [a-z0-9]+ before building blob keys
This commit is contained in:
@@ -9,14 +9,28 @@ import (
|
|||||||
_ "image/png"
|
_ "image/png"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/disintegration/imaging"
|
"github.com/disintegration/imaging"
|
||||||
"gocloud.dev/blob"
|
"gocloud.dev/blob"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// thumbToken is the alphabet allowed for the mode and extension segments of a
|
||||||
|
// thumb filename. Both are interpolated into a blob key, which fileblob maps
|
||||||
|
// to a filesystem path, so separators and dots must never reach it.
|
||||||
|
var thumbToken = regexp.MustCompile(`^[a-z0-9]+$`)
|
||||||
|
|
||||||
// ThumbFilename is Winter File::getThumbFilename: thumb_<id>_<w>_<h>_<ox>_<oy>_<mode>.<ext>.
|
// ThumbFilename is Winter File::getThumbFilename: thumb_<id>_<w>_<h>_<ox>_<oy>_<mode>.<ext>.
|
||||||
|
// A mode or ext outside [a-z0-9]+ is coerced to "auto" / "jpg" so the result
|
||||||
|
// is always a single safe path element; File.Thumb rejects such input instead.
|
||||||
func ThumbFilename(id uint, w, h int, offsetX, offsetY int, mode, ext string) string {
|
func ThumbFilename(id uint, w, h int, offsetX, offsetY int, mode, ext string) string {
|
||||||
|
if !thumbToken.MatchString(mode) {
|
||||||
|
mode = "auto"
|
||||||
|
}
|
||||||
|
if !thumbToken.MatchString(ext) {
|
||||||
|
ext = "jpg"
|
||||||
|
}
|
||||||
return fmt.Sprintf("thumb_%d_%d_%d_%d_%d_%s.%s", id, w, h, offsetX, offsetY, mode, ext)
|
return fmt.Sprintf("thumb_%d_%d_%d_%d_%d_%s.%s", id, w, h, offsetX, offsetY, mode, ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +106,14 @@ func (f *File) Thumb(ctx context.Context, bucket *blob.Bucket, w, h int, mode st
|
|||||||
if mode == "" {
|
if mode == "" {
|
||||||
mode = "auto"
|
mode = "auto"
|
||||||
}
|
}
|
||||||
|
mode = strings.ToLower(mode)
|
||||||
|
if !thumbToken.MatchString(mode) {
|
||||||
|
return "", fmt.Errorf("attach: invalid thumb mode %q", mode)
|
||||||
|
}
|
||||||
ext := fileExt(f.DiskName)
|
ext := fileExt(f.DiskName)
|
||||||
|
if !thumbToken.MatchString(ext) {
|
||||||
|
return "", fmt.Errorf("attach: invalid thumb extension %q", ext)
|
||||||
|
}
|
||||||
thumbName := ThumbFilename(f.ID, w, h, 0, 0, mode, ext)
|
thumbName := ThumbFilename(f.ID, w, h, 0, 0, mode, ext)
|
||||||
part := PartitionDirectory(f.DiskName)
|
part := PartitionDirectory(f.DiskName)
|
||||||
thumbKey := part + thumbName
|
thumbKey := part + thumbName
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gocloud.dev/blob"
|
"gocloud.dev/blob"
|
||||||
@@ -19,6 +21,43 @@ func TestThumbFilename(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestThumbFilenameRejectsUnsafeTokens(t *testing.T) {
|
||||||
|
for _, tc := range []struct{ mode, ext string }{
|
||||||
|
{"../../secret", "jpg"},
|
||||||
|
{"auto", "../jpg"},
|
||||||
|
{"a/b", "jpg"},
|
||||||
|
{"auto", "j.pg"},
|
||||||
|
{"", ""},
|
||||||
|
{"Crop", "JPG"},
|
||||||
|
} {
|
||||||
|
got := ThumbFilename(42, 200, 200, 0, 0, tc.mode, tc.ext)
|
||||||
|
if strings.ContainsAny(got, `/\`) || strings.Contains(got, "..") {
|
||||||
|
t.Fatalf("mode=%q ext=%q produced unsafe name %q", tc.mode, tc.ext, got)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(got, "thumb_42_200_200_0_0_") {
|
||||||
|
t.Fatalf("mode=%q ext=%q name %q lost its prefix", tc.mode, tc.ext, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if got := ThumbFilename(42, 200, 200, 0, 0, "../../secret", "jpg"); got != "thumb_42_200_200_0_0_auto.jpg" {
|
||||||
|
t.Fatalf("unsafe mode coerced to %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileThumbRejectsTraversalMode(t *testing.T) {
|
||||||
|
bucket := memblob.OpenBucket(nil)
|
||||||
|
defer bucket.Close()
|
||||||
|
f := &File{ID: 42, DiskName: "abc123xyz789.jpg"}
|
||||||
|
for _, mode := range []string{"../../secret", "a/b", "auto.jpg", "cr op"} {
|
||||||
|
if _, err := f.Thumb(t.Context(), bucket, 200, 200, mode); err == nil {
|
||||||
|
t.Fatalf("mode %q must be rejected", mode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iter := bucket.List(nil)
|
||||||
|
if obj, err := iter.Next(t.Context()); err != io.EOF {
|
||||||
|
t.Fatalf("rejected modes must write nothing, found %v (err %v)", obj, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPartitionDirectory(t *testing.T) {
|
func TestPartitionDirectory(t *testing.T) {
|
||||||
got := PartitionDirectory("abc123xyz.jpg")
|
got := PartitionDirectory("abc123xyz.jpg")
|
||||||
const want = "abc/123/xyz/"
|
const want = "abc/123/xyz/"
|
||||||
|
|||||||
Reference in New Issue
Block a user