diff --git a/backend/go.mod b/backend/go.mod index 09e151a5..fd535ed1 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -38,6 +38,7 @@ require ( github.com/oschwald/maxminddb-golang/v2 v2.2.0 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 + github.com/zitadel/exifremove v0.1.0 go.opentelemetry.io/contrib/bridges/otelslog v0.18.0 go.opentelemetry.io/contrib/exporters/autoexport v0.68.0 go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.68.0 @@ -83,25 +84,37 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/disintegration/gift v1.2.1 // indirect + github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect + github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect + github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 // indirect + github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 // indirect + github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect + github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect + github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect + github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.2 // indirect github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect + github.com/go-errors/errors v1.5.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/go-webauthn/x v0.2.5 // indirect + github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7 // indirect github.com/google/go-github/v39 v39.2.0 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect + github.com/h2non/filetype v1.1.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect @@ -169,6 +182,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.71.0 // indirect modernc.org/mathutil v1.7.1 // indirect diff --git a/backend/go.sum b/backend/go.sum index 6ce103bc..39cbd15f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -89,6 +89,32 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk= +github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d/go.mod h1:lOaOt7+UEppOgyvRy749v3do836U/hw0YVJNjoyPaEs= +github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= +github.com/dsoprea/go-exif/v2 v2.0.0-20200604193436-ca8584a0e1c4/go.mod h1:9EXlPeHfblFFnwu5UOqmP2eoZfJyAZ2Ri/Vki33ajO0= +github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d h1:yeH8wrJa3+8uKKDAdURHUK1ds2UvKhMqX2MiOdVeKPs= +github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d/go.mod h1:oKrjk2kb3rAR5NbtSTLUMvMSbc+k8ZosI3MaVH47noc= +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-20210512043655-120bcdb2a55e/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= +github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= +github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8= +github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102 h1:P1dsxzctGkmG6Zf7gH2xrZhNXWP5/FuLDI7xbCGsWTo= +github.com/dsoprea/go-jpeg-image-structure v0.0.0-20221012074422-4f3f7e934102/go.mod h1:6+tQXZ+I62x13UZ+hemLVoZIuq/usVzvau7bqwUo9P0= +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/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= +github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d h1:dg6UMHa50VI01WuPWXPbNJpO8QSyvIF5T5n2IZiqX3A= +github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d/go.mod h1:pqKB+ijp27cEcrHxhXVgUUMlSDRuGJJp1E+20Lj5H0E= +github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d h1:8+qI8ant/vZkNSsbwSjIR6XJfWcDVTg/qx/3pRUUZNA= +github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d/go.mod h1:yTR3tKgyk20phAFg6IE9ulMA5NjEDD2wyx+okRFLVtw= +github.com/dsoprea/go-utility v0.0.0-20200711062821-fab8125e9bdf/go.mod h1:95+K3z2L0mqsVYd6yveIv1lmtT3tcQQ3dVakPySffW8= +github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 h1:/py11NlxDaOxkT9OKN+gXgT+QOH5xj1ZRoyusfRIlo4= +github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349/go.mod h1:KVK+/Hul09ujXAGq+42UBgCTnXkiJZRnLYdURGjQUwo= +github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uAzdkPTub5Y9yQwXe8W4m2XuP0tK4a9Q/dantD0+uaU= github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1 h1:RW22Y3QjGrb97NUA8yupdFcaqg//+hMI2fZrETBvQ4s= github.com/dunglas/go-urlpattern v0.0.0-20241020164140-716dfa1c80b1/go.mod h1:mnVcdqOeYg0HvT6veRo7wINa1mJ+lC/R4ig2lWcapSI= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -119,6 +145,11 @@ github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-co-op/gocron/v2 v2.21.1 h1:QYOK6iOQVCut+jDcs4zRdWRTBHRxRCEeeFi1TnAmgbU= github.com/go-co-op/gocron/v2 v2.21.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U= +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/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-ldap/ldap/v3 v3.4.13 h1:+x1nG9h+MZN7h/lUi5Q3UZ0fJ1GyDQYbPvbuH38baDQ= github.com/go-ldap/ldap/v3 v3.4.13/go.mod h1:LxsGZV6vbaK0sIvYfsv47rfh4ca0JXokCoKjZxsszv0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -140,6 +171,9 @@ github.com/go-webauthn/webauthn v0.17.3 h1:XHZ0TXV7k8vChcE4TFgPitOPJ5cb7h1dpAeFD github.com/go-webauthn/webauthn v0.17.3/go.mod h1:PlkMgmuL9McCT7dvgBj/Sz/fgs3V6ZID6/KnFkEcPvQ= github.com/go-webauthn/x v0.2.5 h1:wEVTfU04XFyPTXGQbKOQwMKhcDWfDAkdsDDBsDaG9yY= github.com/go-webauthn/x v0.2.5/go.mod h1:Qna/yJz9rV6lRzwl5BfYbmTJpVGxcBIds3gJtw2tlGg= +github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= +github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= +github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= @@ -150,6 +184,10 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= +github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/geo v0.0.0-20200319012246-673a6f80352d/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= +github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7 h1:kG/6mhO8OwbQrA/0XEPwKJs3D3jG0m1rNH/ZRKDA/pQ= +github.com/golang/geo v0.0.0-20250319145452-ed1c8b99c3d7/go.mod h1:J+F9/3Ofc8ysEOY2/cNjxTMl2eB1gvPIywEHUplPgDA= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -175,6 +213,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= +github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= +github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -201,6 +241,7 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -334,6 +375,8 @@ github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz3 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zitadel/exifremove v0.1.0 h1:qD50ezWsfeeqfcvs79QyyjVfK+snN12v0U0deaU8aKg= +github.com/zitadel/exifremove v0.1.0/go.mod h1:rzKJ3woL/Rz2KthVBiSBKIBptNTvgmk9PLaeUKTm+ek= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -422,6 +465,11 @@ golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-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-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -445,6 +493,7 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -504,6 +553,10 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.7/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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/backend/internal/service/app_images_service.go b/backend/internal/service/app_images_service.go index 3d0adb90..4f9ea447 100644 --- a/backend/internal/service/app_images_service.go +++ b/backend/internal/service/app_images_service.go @@ -12,6 +12,7 @@ import ( "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/storage" "github.com/pocket-id/pocket-id/backend/internal/utils" + imageutil "github.com/pocket-id/pocket-id/backend/internal/utils/image" ) type AppImagesService struct { @@ -68,7 +69,12 @@ func (s *AppImagesService) UpdateImage(ctx context.Context, file *multipart.File } defer fileReader.Close() - if err := s.storage.Save(ctx, imagePath, fileReader); err != nil { + strippedReader, err := imageutil.StripMetadata(fileReader, fileType) + if err != nil { + return err + } + + if err := s.storage.Save(ctx, imagePath, strippedReader); err != nil { return err } diff --git a/backend/internal/service/app_images_service_test.go b/backend/internal/service/app_images_service_test.go index 006f0c43..bca7b4b6 100644 --- a/backend/internal/service/app_images_service_test.go +++ b/backend/internal/service/app_images_service_test.go @@ -3,6 +3,7 @@ package service import ( "bytes" "context" + "encoding/binary" "io" "io/fs" "mime/multipart" @@ -56,6 +57,29 @@ func TestAppImagesService_UpdateImage(t *testing.T) { require.ErrorIs(t, err, fs.ErrNotExist) } +func TestAppImagesService_UpdateImageStripsMetadata(t *testing.T) { + store, err := storage.NewFilesystemStorage(t.TempDir()) + require.NoError(t, err) + + service := NewAppImagesService(map[string]string{}, store) + + fileHeader := newFileHeader(t, "logo.webp", webpFile( + webpChunk("VP8 ", []byte{1, 2, 3, 4}), + webpChunk("EXIF", []byte("secret")), + )) + + require.NoError(t, service.UpdateImage(context.Background(), fileHeader, "logoLight")) + + reader, _, err := store.Open(context.Background(), path.Join("application-images", "logoLight.webp")) + require.NoError(t, err) + defer reader.Close() + + payload, err := io.ReadAll(reader) + require.NoError(t, err) + assert.NotContains(t, string(payload), "secret") + assert.Contains(t, string(payload), "VP8 ") +} + func TestAppImagesService_ErrorsAndFlags(t *testing.T) { store, err := storage.NewFilesystemStorage(t.TempDir()) require.NoError(t, err) @@ -112,3 +136,26 @@ func newFileHeader(t *testing.T, filename string, content []byte) *multipart.Fil return fileHeader } + +func webpFile(chunks ...[]byte) []byte { + var out bytes.Buffer + out.WriteString("RIFF") + out.Write([]byte{0, 0, 0, 0}) + out.WriteString("WEBP") + for _, chunk := range chunks { + out.Write(chunk) + } + binary.LittleEndian.PutUint32(out.Bytes()[4:8], uint32(out.Len()-8)) //nolint:gosec + return out.Bytes() +} + +func webpChunk(chunkType string, data []byte) []byte { + var out bytes.Buffer + out.WriteString(chunkType) + _ = binary.Write(&out, binary.LittleEndian, uint32(len(data))) //nolint:gosec + out.Write(data) + if len(data)%2 == 1 { + out.WriteByte(0) + } + return out.Bytes() +} diff --git a/backend/internal/service/oidc_service.go b/backend/internal/service/oidc_service.go index c71dc45c..d58d4c49 100644 --- a/backend/internal/service/oidc_service.go +++ b/backend/internal/service/oidc_service.go @@ -1,7 +1,6 @@ package service import ( - "bytes" "context" "crypto/sha256" "crypto/subtle" @@ -35,6 +34,7 @@ import ( datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" "github.com/pocket-id/pocket-id/backend/internal/storage" "github.com/pocket-id/pocket-id/backend/internal/utils" + imageutil "github.com/pocket-id/pocket-id/backend/internal/utils/image" ) const ( @@ -746,11 +746,10 @@ func (s *OidcService) introspectRefreshToken(ctx context.Context, clientID strin ). First(&storedRefreshToken). Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - introspectDto.Active = false - return introspectDto, nil - } + if errors.Is(err, gorm.ErrRecordNotFound) { + introspectDto.Active = false + return introspectDto, nil + } else if err != nil { return introspectDto, err } @@ -1064,7 +1063,12 @@ func (s *OidcService) UpdateClientLogo(ctx context.Context, clientID string, fil return err } defer reader.Close() - err = s.fileStorage.Save(ctx, imagePath, reader) + strippedReader, err := imageutil.StripMetadata(reader, fileType) + if err != nil { + return err + } + + err = s.fileStorage.Save(ctx, imagePath, strippedReader) if err != nil { return err } @@ -2180,20 +2184,19 @@ func (s *OidcService) downloadAndSaveLogoFromURL(parentCtx context.Context, clie darkSuffix = "-dark" } - // Buffer the body so that storage backends receive a seekable reader with a known content length, - // which is required for correct checksum calculation on S3-compatible services - limitedBody := utils.NewLimitReader(resp.Body, maxLogoSize+1) - buf, err := io.ReadAll(limitedBody) + limitReader := utils.NewLimitReader(resp.Body, maxLogoSize+1) + strippedReader, err := imageutil.StripMetadata(limitReader, ext) if errors.Is(err, utils.ErrSizeExceeded) { - if errors.Is(err, utils.ErrSizeExceeded) { - return errLogoTooLarge - } else if err != nil { - return err - } + return errLogoTooLarge + } else if err != nil { + return err } imagePath := path.Join("oidc-client-images", clientID+darkSuffix+"."+ext) - if err = s.fileStorage.Save(ctx, imagePath, bytes.NewReader(buf)); err != nil { + err = s.fileStorage.Save(ctx, imagePath, strippedReader) + if errors.Is(err, utils.ErrSizeExceeded) { + return errLogoTooLarge + } else if err != nil { return err } diff --git a/backend/internal/utils/image/metadata.go b/backend/internal/utils/image/metadata.go new file mode 100644 index 00000000..1ea59a77 --- /dev/null +++ b/backend/internal/utils/image/metadata.go @@ -0,0 +1,106 @@ +package profilepicture + +import ( + "bytes" + "encoding/binary" + "io" + "strings" + + "github.com/zitadel/exifremove/pkg/exifremove" +) + +const maxRIFFSize = ^uint32(0) + +// StripMetadata removes EXIF/XMP metadata from JPG, PNG and WEBP images +// Returns a *bytes.Reader so that storage backends receive a seekable reader with a known content length, +// which is required for correct checksum calculation on S3-compatible services +func StripMetadata(file io.Reader, ext string) (*bytes.Reader, error) { + data, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + switch strings.ToLower(ext) { + case "jpg", "jpeg": + stripped, err := exifremove.Remove(data) + if err == nil { + return bytes.NewReader(stripped), nil + } + return bytes.NewReader(data), nil + case "png": + stripped, err := exifremove.Remove(data) + if err == nil { + return bytes.NewReader(stripped), nil + } + return bytes.NewReader(data), nil + case "webp": + return bytes.NewReader(stripWEBPMetadata(data)), nil + default: + return bytes.NewReader(data), nil + } +} + +func stripWEBPMetadata(data []byte) []byte { + // Check if the file contains the RIFF...WEBP header + if len(data) < 12 || string(data[:4]) != "RIFF" || string(data[8:12]) != "WEBP" { + return data + } + + var out bytes.Buffer + // Build the WEBP header + out.WriteString("RIFF") + out.Write([]byte{0, 0, 0, 0}) // Size will be filled at the end + out.WriteString("WEBP") + + for pos := 12; pos < len(data); { + // Each RIFF chunk needs an 8 byte header + if pos+8 > len(data) { + return data + } + + // Read the chunk type and payload size from the header + chunkType := string(data[pos : pos+4]) + chunkSize := int(binary.LittleEndian.Uint32(data[pos+4 : pos+8])) + chunkEnd := pos + 8 + chunkSize + // Chunks with odd payload sizes include one additional byte at the end + if chunkSize%2 == 1 { + chunkEnd++ + } + + // End of chunk can't be more than the actual image data length + if chunkEnd > len(data) { + return data + } + + // Remove chunks with the EXIF or XMP data type + if chunkType == "EXIF" || chunkType == "XMP " { + pos = chunkEnd + continue + } + + // In the VP8X chunk there is a feature flag if the file contains any EXIF or XMP data + if chunkType == "VP8X" && chunkSize >= 10 { + // Copy the chunk because we don't want to modify the original one + chunk := make([]byte, chunkEnd-pos) + copy(chunk, data[pos:chunkEnd]) + + // Clear the Exif and XMP feature flags + chunk[8] &^= 0x0c + out.Write(chunk) + } else { + out.Write(data[pos:chunkEnd]) + } + + pos = chunkEnd + } + + // WEBP image can max be 4GB in size + riffSize := out.Len() - 8 + if riffSize < 0 || riffSize > int(maxRIFFSize) { + return data + } + + // Set the size in the header (byte 4-7) + binary.LittleEndian.PutUint32(out.Bytes()[4:8], uint32(riffSize)) + return out.Bytes() +} diff --git a/backend/internal/utils/image/metadata_test.go b/backend/internal/utils/image/metadata_test.go new file mode 100644 index 00000000..321e9470 --- /dev/null +++ b/backend/internal/utils/image/metadata_test.go @@ -0,0 +1,81 @@ +package profilepicture + +import ( + "bytes" + "encoding/binary" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestStripMetadata(t *testing.T) { + t.Run("removes WEBP EXIF and XMP chunks", func(t *testing.T) { + input := webpFile( + webpChunk("VP8X", []byte{0x0c, 0, 0, 0, 0, 0, 0, 0, 0, 0}), + webpChunk("VP8 ", []byte{1, 2, 3, 4}), + webpChunk("EXIF", []byte("gps")), + webpChunk("XMP ", []byte("xmp")), + webpChunk("ICCP", []byte("profile")), + ) + + output := stripReader(t, input, "webp") + + assert.Contains(t, string(output), "VP8 ") + assert.NotContains(t, string(output), "gps") + assert.NotContains(t, string(output), "xmp") + assert.Contains(t, string(output), "profile") + assert.Equal(t, byte(0), output[20]&0x0c) + }) + + t.Run("leaves non photo metadata formats unchanged", func(t *testing.T) { + input := []byte(`secret`) + + output := stripReader(t, input, "svg") + + assert.Equal(t, input, output) + }) + + t.Run("leaves malformed photo data unchanged", func(t *testing.T) { + input := []byte("fake-png-content") + + output := stripReader(t, input, "png") + + assert.Equal(t, input, output) + }) +} + +func stripReader(t *testing.T, input []byte, ext string) []byte { + t.Helper() + + reader, err := StripMetadata(bytes.NewReader(input), ext) + require.NoError(t, err) + + output, err := io.ReadAll(reader) + require.NoError(t, err) + return output +} + +func webpFile(chunks ...[]byte) []byte { + var out bytes.Buffer + out.WriteString("RIFF") + out.Write([]byte{0, 0, 0, 0}) + out.WriteString("WEBP") + for _, chunk := range chunks { + out.Write(chunk) + } + binary.LittleEndian.PutUint32(out.Bytes()[4:8], uint32(out.Len()-8)) //nolint:gosec + return out.Bytes() +} + +func webpChunk(chunkType string, data []byte) []byte { + var out bytes.Buffer + out.WriteString(chunkType) + _ = binary.Write(&out, binary.LittleEndian, uint32(len(data))) //nolint:gosec + out.Write(data) + if len(data)%2 == 1 { + out.WriteByte(0) + } + return out.Bytes() +}