feat: strip EXIF information from uploaded avatars (#9638)

Strips EXIF information from uploaded avatars (excluding the orientation tag), affecting both user & repo avatars.  Adds a new subcommand `forgejo admin avatar-strip-exif` to perform a retroactive update of avatar files.

Fixes #9608.

## Checklist

The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
  - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server.
- I added test coverage for JavaScript changes...
  - [ ] in `web_src/js/*.test.js` if it can be unit tested.
  - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)).

### Documentation

- [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [ ] I do not want this change to show in the release notes.
- [ ] I want the title to show in the release notes with a link to this pull request.
- [x] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title.

<!--start release-notes-assistant-->

## Release notes
<!--URL:https://codeberg.org/forgejo/forgejo-->
- Features
  - [PR](https://codeberg.org/forgejo/forgejo/pulls/9638): <!--number 9638 --><!--line 0 --><!--description VXBsb2FkZWQgYXZhdGFyIGltYWdlcyBjYW4gc29tZXRpbWVzIGNvbnRhaW4gdW5leHBlY3RlZCBtZXRhZGF0YSBzdWNoIGFzIHRoZSBsb2NhdGlvbiB3aGVyZSB0aGUgaW1hZ2Ugd2FzIGNyZWF0ZWQsIG9yIHRoZSBkZXZpY2UgdGhlIGltYWdlIHdhcyBjcmVhdGVkIHdpdGgsIHN0b3JlZCBpbiBhIGZvcm1hdCBjYWxsZWQgRVhJRi4gRm9yZ2VqbyBub3cgcmVtb3ZlcyBFWElGIGRhdGEgd2hlbiBjdXN0b20gdXNlciBhbmQgcmVwb3NpdG9yeSBpbWFnZXMgYXJlIHVwbG9hZGVkIGluIG9yZGVyIHRvIHJlZHVjZSB0aGUgcmlzayBvZiBwZXJzb25hbGx5IGlkZW50aWZpYWJsZSBpbmZvcm1hdGlvbiBiZWluZyBsZWFrZWQgdW5leHBlY3RlZGx5LiBBIG5ldyBDTEkgc3ViY29tbWFuZCBgZm9yZ2VqbyBkb2N0b3IgYXZhdGFyLXN0cmlwLWV4aWZgIGNhbiBiZSB1c2VkIHRvIHN0cmlwIEVYSUYgaW5mb3JtYXRpb24gZnJvbSBhbGwgZXhpc3RpbmcgYXZhdGFyczsgd2UgcmVjb21tZW5kIHRoYXQgYWRtaW5pc3RyYXRvcnMgcnVuIHRoaXMgY29tbWFuZCBvbmNlIGFmdGVyIHVwZ3JhZGUgaW4gb3JkZXIgdG8gbWluaW1pemUgdGhpcyByaXNrIGZvciBleGlzdGluZyBzdG9yZWQgZmlsZXMu-->Uploaded avatar images can sometimes contain unexpected metadata such as the location where the image was created, or the device the image was created with, stored in a format called EXIF. Forgejo now removes EXIF data when custom user and repository images are uploaded in order to reduce the risk of personally identifiable information being leaked unexpectedly. A new CLI subcommand `forgejo doctor avatar-strip-exif` can be used to strip EXIF information from all existing avatars; we recommend that administrators run this command once after upgrade in order to minimize this risk for existing stored files.<!--description-->
<!--end release-notes-assistant-->

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9638
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
Mathieu Fenniak 2025-10-13 23:16:17 +02:00 committed by Mathieu Fenniak
parent 8eb8f49581
commit 78d92aafd7
9 changed files with 277 additions and 13 deletions

File diff suppressed because one or more lines are too long

View file

@ -6,6 +6,8 @@ package cmd
import ( import (
"context" "context"
"fmt" "fmt"
"image"
"io"
golog "log" golog "log"
"os" "os"
"path/filepath" "path/filepath"
@ -15,11 +17,15 @@ import (
"forgejo.org/models/db" "forgejo.org/models/db"
"forgejo.org/models/migrations" "forgejo.org/models/migrations"
migrate_base "forgejo.org/models/migrations/base" migrate_base "forgejo.org/models/migrations/base"
repo_model "forgejo.org/models/repo"
user_model "forgejo.org/models/user"
"forgejo.org/modules/container" "forgejo.org/modules/container"
"forgejo.org/modules/log" "forgejo.org/modules/log"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/storage"
"forgejo.org/services/doctor" "forgejo.org/services/doctor"
exif_terminator "code.superseriousbusiness.org/exif-terminator"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
) )
@ -34,6 +40,7 @@ func cmdDoctor() *cli.Command {
cmdDoctorCheck(), cmdDoctorCheck(),
cmdRecreateTable(), cmdRecreateTable(),
cmdDoctorConvert(), cmdDoctorConvert(),
cmdAvatarStripExif(),
}, },
} }
} }
@ -99,6 +106,15 @@ You should back-up your database before doing this and ensure that your database
} }
} }
func cmdAvatarStripExif() *cli.Command {
return &cli.Command{
Name: "avatar-strip-exif",
Usage: "Strip EXIF metadata from all images in the avatar storage",
Before: noDanglingArgs,
Action: runAvatarStripExif,
}
}
func runRecreateTable(stdCtx context.Context, ctx *cli.Command) error { func runRecreateTable(stdCtx context.Context, ctx *cli.Command) error {
stdCtx, cancel := installSignals(stdCtx) stdCtx, cancel := installSignals(stdCtx)
defer cancel() defer cancel()
@ -231,3 +247,78 @@ func runDoctorCheck(stdCtx context.Context, ctx *cli.Command) error {
} }
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks) return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
} }
func runAvatarStripExif(ctx context.Context, c *cli.Command) error {
ctx, cancel := installSignals(ctx)
defer cancel()
if err := initDB(ctx); err != nil {
return err
}
if err := storage.Init(); err != nil {
return err
}
type HasCustomAvatarRelativePath interface {
CustomAvatarRelativePath() string
}
doExifStrip := func(obj HasCustomAvatarRelativePath, name string, target_storage storage.ObjectStorage) error {
if obj.CustomAvatarRelativePath() == "" {
return nil
}
log.Info("Stripping avatar for %s...", name)
avatarFile, err := target_storage.Open(obj.CustomAvatarRelativePath())
if err != nil {
return fmt.Errorf("storage.Avatars.Open: %w", err)
}
_, imgType, err := image.DecodeConfig(avatarFile)
if err != nil {
return fmt.Errorf("image.DecodeConfig: %w", err)
}
// reset io.Reader for exif termination scan
_, err = avatarFile.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("avatarFile.Seek: %w", err)
}
cleanedData, err := exif_terminator.Terminate(avatarFile, imgType)
if err != nil && strings.Contains(err.Error(), "cannot be processed") {
// expected error for an image type that isn't supported by exif_terminator
log.Info("... image type %s is not supported by exif_terminator, skipping.", imgType)
return nil
} else if err != nil {
return fmt.Errorf("error cleaning exif data: %w", err)
}
if err := storage.SaveFrom(target_storage, obj.CustomAvatarRelativePath(), func(w io.Writer) error {
_, err := io.Copy(w, cleanedData)
return err
}); err != nil {
return fmt.Errorf("Failed to create dir %s: %w", obj.CustomAvatarRelativePath(), err)
}
log.Info("... completed %s.", name)
return nil
}
err := db.Iterate(ctx, nil, func(ctx context.Context, user *user_model.User) error {
return doExifStrip(user, fmt.Sprintf("user %s", user.Name), storage.Avatars)
})
if err != nil {
return err
}
err = db.Iterate(ctx, nil, func(ctx context.Context, repo *repo_model.Repository) error {
return doExifStrip(repo, fmt.Sprintf("repo %s", repo.Name), storage.RepoAvatars)
})
if err != nil {
return err
}
return nil
}

12
go.mod
View file

@ -17,6 +17,8 @@ require (
code.forgejo.org/go-chi/session v1.0.2 code.forgejo.org/go-chi/session v1.0.2
code.gitea.io/actions-proto-go v0.4.0 code.gitea.io/actions-proto-go v0.4.0
code.gitea.io/sdk/gitea v0.21.0 code.gitea.io/sdk/gitea v0.21.0
code.superseriousbusiness.org/exif-terminator v0.11.0
code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
connectrpc.com/connect v1.19.1 connectrpc.com/connect v1.19.1
github.com/42wim/httpsig v1.2.3 github.com/42wim/httpsig v1.2.3
@ -34,6 +36,7 @@ require (
github.com/djherbis/buffer v1.2.0 github.com/djherbis/buffer v1.2.0
github.com/djherbis/nio/v3 v3.0.1 github.com/djherbis/nio/v3 v3.0.1
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 github.com/editorconfig/editorconfig-core-go/v2 v2.6.3
github.com/emersion/go-imap v1.2.1 github.com/emersion/go-imap v1.2.1
@ -116,6 +119,7 @@ require (
require ( require (
cloud.google.com/go/compute/metadata v0.6.0 // indirect cloud.google.com/go/compute/metadata v0.6.0 // indirect
code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0 // indirect
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect
git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect
@ -160,6 +164,10 @@ require (
github.com/davidmz/go-pageant v1.0.2 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb // indirect
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
@ -167,6 +175,7 @@ require (
github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-enry/go-oniguruma v1.2.1 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect
github.com/go-errors/errors v1.1.1 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect github.com/go-git/go-billy/v5 v5.6.2 // indirect
@ -176,8 +185,10 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.1 // indirect github.com/go-openapi/swag v0.23.1 // indirect
github.com/go-webauthn/x v0.1.25 // indirect github.com/go-webauthn/x v0.1.25 // indirect
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
@ -250,6 +261,7 @@ require (
golang.org/x/tools v0.37.0 // indirect golang.org/x/tools v0.37.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

38
go.sum
View file

@ -46,6 +46,12 @@ code.gitea.io/actions-proto-go v0.4.0 h1:OsPBPhodXuQnsspG1sQ4eRE1PeoZyofd7+i73zC
code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas= code.gitea.io/actions-proto-go v0.4.0/go.mod h1:mn7Wkqz6JbnTOHQpot3yDeHx+O5C9EGhMEE+htvHBas=
code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
code.superseriousbusiness.org/exif-terminator v0.11.0 h1:Hof0MCcsa+1fS17gf86fTTZ8AQnMY9h9kzcc+2C6mVg=
code.superseriousbusiness.org/exif-terminator v0.11.0/go.mod h1:9sutT1axa/kSdlPLlRFjCNKmyo/KNx8eX3XZvWBlAEY=
code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0 h1:r9uq8StaSHYKJ8DklR9Xy+E9c40G1Z8yj5TRGi8L6+4=
code.superseriousbusiness.org/go-jpeg-image-structure/v2 v2.3.0/go.mod h1:IK1OlR6APjVB3E9tuYGvf0qXMrwP+TrzcHS5rf4wffQ=
code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0 h1:I512jiIeXDC4//2BeSPrRM2ZS4wpBKUaPeTPxakMNGA=
code.superseriousbusiness.org/go-png-image-structure/v2 v2.3.0/go.mod h1:SNHomXNW88o1pFfLHpD4KsCZLfcr4z5dm+xcX5SV10A=
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM= codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
@ -209,6 +215,22 @@ github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E=
github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8=
github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b h1:NgNuLvW/gAFKU30ULWW0gtkCt56JfB7FrZ2zyo0wT8I=
github.com/dsoprea/go-exif/v3 v3.0.0-20210625224831-a6301f85c82b/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk=
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb h1:gwjJjUr6FY7zAWVEueFPrcRHhd9+IK81TcItbqw2du4=
github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM=
github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA=
github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg=
github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8=
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c h1:7j5aWACOzROpr+dvMtu8GnI97g9ShLWD72XIELMgn+c=
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200609050348-3db9b63b202c/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E=
github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e h1:IxIbA7VbCNrwumIYjDoMOdf4KOSkMC6NJE4s8oRbE7E=
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 h1:XVUp6qW3BIkmM3/1EkrHpa6bL56APOynfXcZEmIgOhs= github.com/editorconfig/editorconfig-core-go/v2 v2.6.3 h1:XVUp6qW3BIkmM3/1EkrHpa6bL56APOynfXcZEmIgOhs=
@ -258,6 +280,10 @@ github.com/go-enry/go-enry/v2 v2.9.2 h1:giOQAtCgBX08kosrX818DCQJTCNtKwoPBGu0qb6n
github.com/go-enry/go-enry/v2 v2.9.2/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8= github.com/go-enry/go-enry/v2 v2.9.2/go.mod h1:9yrj4ES1YrbNb1Wb7/PWYr2bpaCXUGRt0uafN0ISyG8=
github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo= github.com/go-enry/go-oniguruma v1.2.1 h1:k8aAMuJfMrqm/56SG2lV9Cfti6tC4x8673aHCcBk+eo=
github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4= github.com/go-enry/go-oniguruma v1.2.1/go.mod h1:bWDhYP+S6xZQgiRL7wlTScFYBe023B6ilRZbCAD5Hf4=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-errors/errors v1.1.1 h1:ljK/pL5ltg3qoN+OtN6yCv9HWSfMwxSx90GJCZQxYNg=
github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@ -291,6 +317,8 @@ github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx
github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k= github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k=
github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88= github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88=
github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY= github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY=
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4/FHQWkvVRmgijNXRfzkIDHh23ggEo=
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
@ -308,6 +336,9 @@ github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9v
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d h1:C/hKUcHT483btRbeGkrRjJz+Zbcj8audldIi9tRJDCc=
github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -404,6 +435,7 @@ github.com/inbucket/html2text v0.9.0 h1:ULJmVcBEMAcmLE+/rN815KG1Fx6+a4HhbUxiDiN+
github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE= github.com/inbucket/html2text v0.9.0/go.mod h1:QDaumzl+/OzlSVbNohhmg+yAy5pKjUjzCKW2BMvztKE=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8= github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8=
github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI= github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@ -724,7 +756,11 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200320220750-118fecf932d8/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -923,8 +959,10 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -10,6 +10,7 @@ import (
"image" "image"
"image/color" "image/color"
"image/png" "image/png"
"io"
_ "image/gif" // for processing gif images _ "image/gif" // for processing gif images
_ "image/jpeg" // for processing jpeg images _ "image/jpeg" // for processing jpeg images
@ -17,6 +18,7 @@ import (
"forgejo.org/modules/avatar/identicon" "forgejo.org/modules/avatar/identicon"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
exif_terminator "code.superseriousbusiness.org/exif-terminator"
"golang.org/x/image/draw" "golang.org/x/image/draw"
_ "golang.org/x/image/webp" // for processing webp images _ "golang.org/x/image/webp" // for processing webp images
@ -66,15 +68,29 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) {
return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight) return nil, fmt.Errorf("image height is too large: %d > %d", imgCfg.Height, setting.Avatar.MaxHeight)
} }
var cleanedBytes []byte
if imgType != "gif" { // "gif" is the only imgType supported above, but not supported by exif_terminator
cleanedData, err := exif_terminator.Terminate(bytes.NewReader(data), imgType)
if err != nil {
return nil, fmt.Errorf("error cleaning exif data: %w", err)
}
cleanedBytes, err = io.ReadAll(cleanedData)
if err != nil {
return nil, fmt.Errorf("error reading cleaned data: %w", err)
}
} else { // gif
cleanedBytes = data
}
// If the origin is small enough, just use it, then APNG could be supported, // If the origin is small enough, just use it, then APNG could be supported,
// otherwise, if the image is processed later, APNG loses animation. // otherwise, if the image is processed later, APNG loses animation.
// And one more thing, webp is not fully supported, for animated webp, image.DecodeConfig works but Decode fails. // And one more thing, webp is not fully supported, for animated webp, image.DecodeConfig works but Decode fails.
// So for animated webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error. // So for animated webp, if the uploaded file is smaller than maxOriginSize, it will be used, if it's larger, there will be an error.
if len(data) < int(maxOriginSize) { if len(data) < int(maxOriginSize) {
return data, nil return cleanedBytes, nil
} }
img, _, err := image.Decode(bytes.NewReader(data)) img, _, err := image.Decode(bytes.NewReader(cleanedBytes))
if err != nil { if err != nil {
return nil, fmt.Errorf("image.Decode: %w", err) return nil, fmt.Errorf("image.Decode: %w", err)
} }
@ -94,7 +110,7 @@ func processAvatarImage(data []byte, maxOriginSize int64) ([]byte, error) {
// usually the png compression is not good enough, use the original image (no cropping/resizing) if the origin is smaller // usually the png compression is not good enough, use the original image (no cropping/resizing) if the origin is smaller
if len(data) <= len(resized) { if len(data) <= len(resized) {
return data, nil return cleanedBytes, nil
} }
return resized, nil return resized, nil

View file

@ -11,7 +11,10 @@ import (
"testing" "testing"
"forgejo.org/modules/setting" "forgejo.org/modules/setting"
"forgejo.org/modules/test"
jpegstructure "code.superseriousbusiness.org/go-jpeg-image-structure/v2"
"github.com/dsoprea/go-exif/v3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -30,8 +33,8 @@ func Test_RandomImage(t *testing.T) {
} }
func Test_ProcessAvatarPNG(t *testing.T) { func Test_ProcessAvatarPNG(t *testing.T) {
setting.Avatar.MaxWidth = 4096 defer test.MockVariableValue(&setting.Avatar.MaxWidth, 4096)()
setting.Avatar.MaxHeight = 4096 defer test.MockVariableValue(&setting.Avatar.MaxHeight, 4096)()
data, err := os.ReadFile("testdata/avatar.png") data, err := os.ReadFile("testdata/avatar.png")
require.NoError(t, err) require.NoError(t, err)
@ -41,8 +44,8 @@ func Test_ProcessAvatarPNG(t *testing.T) {
} }
func Test_ProcessAvatarJPEG(t *testing.T) { func Test_ProcessAvatarJPEG(t *testing.T) {
setting.Avatar.MaxWidth = 4096 defer test.MockVariableValue(&setting.Avatar.MaxWidth, 4096)()
setting.Avatar.MaxHeight = 4096 defer test.MockVariableValue(&setting.Avatar.MaxHeight, 4096)()
data, err := os.ReadFile("testdata/avatar.jpeg") data, err := os.ReadFile("testdata/avatar.jpeg")
require.NoError(t, err) require.NoError(t, err)
@ -51,17 +54,28 @@ func Test_ProcessAvatarJPEG(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
func Test_ProcessAvatarGIF(t *testing.T) {
defer test.MockVariableValue(&setting.Avatar.MaxWidth, 4096)()
defer test.MockVariableValue(&setting.Avatar.MaxHeight, 4096)()
data, err := os.ReadFile("testdata/avatar.gif")
require.NoError(t, err)
_, err = processAvatarImage(data, 262144)
require.NoError(t, err)
}
func Test_ProcessAvatarInvalidData(t *testing.T) { func Test_ProcessAvatarInvalidData(t *testing.T) {
setting.Avatar.MaxWidth = 5 defer test.MockVariableValue(&setting.Avatar.MaxWidth, 5)()
setting.Avatar.MaxHeight = 5 defer test.MockVariableValue(&setting.Avatar.MaxHeight, 5)()
_, err := processAvatarImage([]byte{}, 12800) _, err := processAvatarImage([]byte{}, 12800)
assert.EqualError(t, err, "image.DecodeConfig: image: unknown format") assert.EqualError(t, err, "image.DecodeConfig: image: unknown format")
} }
func Test_ProcessAvatarInvalidImageSize(t *testing.T) { func Test_ProcessAvatarInvalidImageSize(t *testing.T) {
setting.Avatar.MaxWidth = 5 defer test.MockVariableValue(&setting.Avatar.MaxWidth, 5)()
setting.Avatar.MaxHeight = 5 defer test.MockVariableValue(&setting.Avatar.MaxHeight, 5)()
data, err := os.ReadFile("testdata/avatar.png") data, err := os.ReadFile("testdata/avatar.png")
require.NoError(t, err) require.NoError(t, err)
@ -71,8 +85,8 @@ func Test_ProcessAvatarInvalidImageSize(t *testing.T) {
} }
func Test_ProcessAvatarImage(t *testing.T) { func Test_ProcessAvatarImage(t *testing.T) {
setting.Avatar.MaxWidth = 4096 defer test.MockVariableValue(&setting.Avatar.MaxWidth, 4096)()
setting.Avatar.MaxHeight = 4096 defer test.MockVariableValue(&setting.Avatar.MaxHeight, 4096)()
scaledSize := DefaultAvatarSize * setting.Avatar.RenderedSizeFactor scaledSize := DefaultAvatarSize * setting.Avatar.RenderedSizeFactor
newImgData := func(size int, optHeight ...int) []byte { newImgData := func(size int, optHeight ...int) []byte {
@ -135,3 +149,40 @@ func Test_ProcessAvatarImage(t *testing.T) {
_, err = processAvatarImage(origin, 262144) _, err = processAvatarImage(origin, 262144)
require.ErrorContains(t, err, "image width is too large: 10 > 5") require.ErrorContains(t, err, "image width is too large: 10 > 5")
} }
func safeExifJpeg(t *testing.T, jpeg []byte) {
t.Helper()
parser := jpegstructure.NewJpegMediaParser()
mediaContext, err := parser.ParseBytes(jpeg)
require.NoError(t, err)
sl := mediaContext.(*jpegstructure.SegmentList)
rootIfd, _, err := sl.Exif()
require.NoError(t, err)
err = rootIfd.EnumerateTagsRecursively(func(ifd *exif.Ifd, ite *exif.IfdTagEntry) error {
assert.Equal(t, "Orientation", ite.TagName(), "only Orientation EXIF tag expected")
return nil
})
require.NoError(t, err)
}
func Test_ProcessAvatarExif(t *testing.T) {
t.Run("greater than max origin size", func(t *testing.T) {
data, err := os.ReadFile("testdata/exif.jpg")
require.NoError(t, err)
processedData, err := processAvatarImage(data, 12800)
require.NoError(t, err)
safeExifJpeg(t, processedData)
})
t.Run("smaller than max origin size", func(t *testing.T) {
data, err := os.ReadFile("testdata/exif.jpg")
require.NoError(t, err)
processedData, err := processAvatarImage(data, 128000)
require.NoError(t, err)
safeExifJpeg(t, processedData)
})
}

BIN
modules/avatar/testdata/avatar.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 B

BIN
modules/avatar/testdata/exif.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

1
release-notes/9638.md Normal file
View file

@ -0,0 +1 @@
Uploaded avatar images can sometimes contain unexpected metadata such as the location where the image was created, or the device the image was created with, stored in a format called EXIF. Forgejo now removes EXIF data when custom user and repository images are uploaded in order to reduce the risk of personally identifiable information being leaked unexpectedly. A new CLI subcommand `forgejo doctor avatar-strip-exif` can be used to strip EXIF information from all existing avatars; we recommend that administrators run this command once after upgrade in order to minimize this risk for existing stored files.