From eb1a719fc3de0638f80e97d0379e75068f84abed Mon Sep 17 00:00:00 2001 From: Stephen Adamson Date: Fri, 20 Feb 2026 17:22:43 +0000 Subject: [PATCH] Exif stripping --- backend/go.mod | 11 +++++-- backend/go.sum | 26 +++++++++++----- backend/handlers/portfolio/images.go | 36 ++++++++++++++++++++++ frontend/src/routes/portfolio/+page.svelte | 6 ++++ 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/backend/go.mod b/backend/go.mod index fded932..86d6a35 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -8,7 +8,8 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.19.8 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 github.com/go-chi/jwtauth/v5 v5.3.3 - golang.org/x/text v0.30.0 + github.com/kovidgoyal/imaging v1.8.19 + golang.org/x/text v0.33.0 ) require ( @@ -30,8 +31,12 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/kovidgoyal/go-parallel v1.1.1 // indirect + github.com/kovidgoyal/go-shm v1.0.0 // indirect + github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect - golang.org/x/sync v0.17.0 // indirect + golang.org/x/image v0.35.0 // indirect + golang.org/x/sync v0.19.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) @@ -49,5 +54,5 @@ require ( github.com/nyaruka/phonenumbers v1.6.6 github.com/segmentio/asm v1.2.1 // indirect golang.org/x/crypto v0.43.0 - golang.org/x/sys v0.37.0 // indirect + golang.org/x/sys v0.40.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index b1cd889..31b7991 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -57,6 +57,12 @@ github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kovidgoyal/go-parallel v1.1.1 h1:1OzpNjtrUkBPq3UaqrnvOoB2F9RttSt811uiUXyI7ok= +github.com/kovidgoyal/go-parallel v1.1.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw= +github.com/kovidgoyal/go-shm v1.0.0 h1:HJEel9D1F9YhULvClEHJLawoRSj/1u/EDV7MJbBPgQo= +github.com/kovidgoyal/go-shm v1.0.0/go.mod h1:Yzb80Xf9L3kaoB2RGok9hHwMIt7Oif61kT6t3+VnZds= +github.com/kovidgoyal/imaging v1.8.19 h1:zWJdQqF2tfSKjvoB7XpLRhVGbYsze++M0iaqZ4ZkhNk= +github.com/kovidgoyal/imaging v1.8.19/go.mod h1:I0q8RdoEuyc4G8GFOF9CaluTUHQSf68d6TmsqpvfRI8= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= @@ -73,6 +79,8 @@ github.com/nyaruka/phonenumbers v1.6.6 h1:cZv5/vslJh65zuOrLjdVDHKHzVEwVuUsXAPQi3 github.com/nyaruka/phonenumbers v1.6.6/go.mod h1:7gjs+Lchqm49adhAKB5cdcng5ZXgt6x7Jgvi0ZorUtU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -80,18 +88,20 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA= golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= +golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/handlers/portfolio/images.go b/backend/handlers/portfolio/images.go index e82b0dc..68184d2 100644 --- a/backend/handlers/portfolio/images.go +++ b/backend/handlers/portfolio/images.go @@ -18,10 +18,30 @@ import ( "time" "github.com/go-chi/chi/v5" + "github.com/kovidgoyal/imaging" ) const MaxInputLength = 256 +// processImage strips metadata and auto-orients the image +func processImage(data []byte, quality int) ([]byte, error) { + // Decode the image - this automatically applies EXIF orientation + // and strips all metadata (EXIF, GPS, etc.) + img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true)) + if err != nil { + return nil, fmt.Errorf("failed to decode image: %w", err) + } + + // Encode to JPEG without any metadata + var buf bytes.Buffer + err = imaging.Encode(&buf, img, imaging.JPEG, imaging.JPEGQuality(quality)) + if err != nil { + return nil, fmt.Errorf("failed to encode image: %w", err) + } + + return buf.Bytes(), nil +} + // validateInputLength returns an error if input exceeds max length func validateInputLength(input string) error { if len(input) > MaxInputLength { @@ -561,6 +581,14 @@ func UploadImage(w http.ResponseWriter, r *http.Request) { return } + // Strip metadata (EXIF, GPS, camera info) and auto-orient + fileBytes, err = processImage(fileBytes, 85) + if err != nil { + log.Printf("Failed to process image: %v", err) + http.Error(w, "Failed to process image", http.StatusInternalServerError) + return + } + thumbBytes, err := io.ReadAll(thumbFile) if err != nil { log.Printf("Failed to read thumbnail: %v", err) @@ -568,6 +596,14 @@ func UploadImage(w http.ResponseWriter, r *http.Request) { return } + // Strip metadata from thumbnail too + thumbBytes, err = processImage(thumbBytes, 75) + if err != nil { + log.Printf("Failed to process thumbnail: %v", err) + http.Error(w, "Failed to process thumbnail", http.StatusInternalServerError) + return + } + bucket := "crussell" if err := s3.Client.Upload(r.Context(), bucket, key, bytes.NewReader(fileBytes)); err != nil { diff --git a/frontend/src/routes/portfolio/+page.svelte b/frontend/src/routes/portfolio/+page.svelte index 166f325..5e86d0c 100644 --- a/frontend/src/routes/portfolio/+page.svelte +++ b/frontend/src/routes/portfolio/+page.svelte @@ -255,6 +255,12 @@ // Build URL with reactive page state const url = new URL(page.url); + + // Remove all existing filter params first to avoid stale params + const keysToDelete = Array.from(url.searchParams.keys()).filter((k) => k.startsWith('filter[')); + keysToDelete.forEach((k) => url.searchParams.delete(k)); + + // Add current filters for (const [cat, val] of Object.entries(selectedFilters)) { if (val) { url.searchParams.set(`filter[${cat}]`, val);