Tighten avatar decode and encoded size limits.
Reject images over 1024px before pixel decode, resize down to 512 for storage, and cap re-encoded output at the upload byte limit.
This commit is contained in:
+49
-6
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/image/draw"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"plumber/internal/blob"
|
||||
@@ -107,8 +108,8 @@ func (s *Server) handleProfile(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/profile", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// prepareAvatar reads at most maxBytes, sniffs/decodes the image, and re-encodes
|
||||
// it so only valid image bytes are stored publicly.
|
||||
// prepareAvatar reads at most maxBytes, sniffs/decodes the image, resizes to a
|
||||
// small avatar, and re-encodes so only bounded valid image bytes are stored.
|
||||
func prepareAvatar(r io.Reader, maxBytes int64) (body []byte, ext, contentType string, err error) {
|
||||
limited := io.LimitReader(r, maxBytes+1)
|
||||
raw, err := io.ReadAll(limited)
|
||||
@@ -135,9 +136,10 @@ func prepareAvatar(r io.Reader, maxBytes int64) (body []byte, ext, contentType s
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
const maxDim = 4096
|
||||
const maxPixels = 4096 * 4096
|
||||
if cfg.Width <= 0 || cfg.Height <= 0 || cfg.Width > maxDim || cfg.Height > maxDim {
|
||||
// Cap decoded size before allocating pixel buffers (~4 MiB RGBA at 1024²).
|
||||
const maxDecodeDim = 1024
|
||||
const maxPixels = maxDecodeDim * maxDecodeDim
|
||||
if cfg.Width <= 0 || cfg.Height <= 0 || cfg.Width > maxDecodeDim || cfg.Height > maxDecodeDim {
|
||||
return nil, "", "", fmt.Errorf("image dimensions out of range")
|
||||
}
|
||||
if int64(cfg.Width)*int64(cfg.Height) > maxPixels {
|
||||
@@ -152,23 +154,64 @@ func prepareAvatar(r io.Reader, maxBytes int64) (body []byte, ext, contentType s
|
||||
decodedFormat = format
|
||||
}
|
||||
|
||||
const maxAvatarDim = 512
|
||||
img = fitAvatar(img, maxAvatarDim)
|
||||
|
||||
var out bytes.Buffer
|
||||
switch decodedFormat {
|
||||
case "jpeg":
|
||||
if err := jpeg.Encode(&out, img, &jpeg.Options{Quality: 90}); err != nil {
|
||||
if err := jpeg.Encode(&out, img, &jpeg.Options{Quality: 85}); err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
if int64(out.Len()) > maxBytes {
|
||||
return nil, "", "", fmt.Errorf("encoded avatar too large")
|
||||
}
|
||||
return out.Bytes(), ".jpg", "image/jpeg", nil
|
||||
case "png", "webp":
|
||||
if err := png.Encode(&out, img); err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
if int64(out.Len()) > maxBytes {
|
||||
// Fall back to JPEG when PNG balloons past the upload cap.
|
||||
out.Reset()
|
||||
if err := jpeg.Encode(&out, img, &jpeg.Options{Quality: 85}); err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
if int64(out.Len()) > maxBytes {
|
||||
return nil, "", "", fmt.Errorf("encoded avatar too large")
|
||||
}
|
||||
return out.Bytes(), ".jpg", "image/jpeg", nil
|
||||
}
|
||||
return out.Bytes(), ".png", "image/png", nil
|
||||
default:
|
||||
return nil, "", "", fmt.Errorf("unsupported format %s", decodedFormat)
|
||||
}
|
||||
}
|
||||
|
||||
// fitAvatar scales img down so both sides are at most maxDim.
|
||||
func fitAvatar(img image.Image, maxDim int) image.Image {
|
||||
b := img.Bounds()
|
||||
w, h := b.Dx(), b.Dy()
|
||||
if w <= maxDim && h <= maxDim {
|
||||
return img
|
||||
}
|
||||
scale := float64(maxDim) / float64(w)
|
||||
if float64(h)*scale > float64(maxDim) {
|
||||
scale = float64(maxDim) / float64(h)
|
||||
}
|
||||
nw := int(float64(w) * scale)
|
||||
nh := int(float64(h) * scale)
|
||||
if nw < 1 {
|
||||
nw = 1
|
||||
}
|
||||
if nh < 1 {
|
||||
nh = 1
|
||||
}
|
||||
dst := image.NewRGBA(image.Rect(0, 0, nw, nh))
|
||||
draw.CatmullRom.Scale(dst, dst.Bounds(), img, b, draw.Over, nil)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (s *Server) renderProfile(w http.ResponseWriter, r *http.Request, u *store.User, errMsg, stateVal string) {
|
||||
var (
|
||||
questions []store.RankedQuestion
|
||||
|
||||
Reference in New Issue
Block a user