diff --git a/.env.example b/.env.example index 0afd1375..fd3cfb2f 100644 --- a/.env.example +++ b/.env.example @@ -15,4 +15,4 @@ ENCRYPTION_KEY= TRUST_PROXY=false MAXMIND_LICENSE_KEY= PUID=1000 -PGID=1000 \ No newline at end of file +PGID=1000 diff --git a/.github/workflows/backend-linter.yml b/.github/workflows/backend-linter.yml index a3284962..0b300198 100644 --- a/.github/workflows/backend-linter.yml +++ b/.github/workflows/backend-linter.yml @@ -33,7 +33,7 @@ jobs: go-version-file: backend/go.mod - name: Run Golangci-lint - uses: golangci/golangci-lint-action@v9.2.1 + uses: golangci/golangci-lint-action@v9.3.0 with: version: v2.12.2 args: --config=.golangci.yml diff --git a/.github/workflows/build-next.yml b/.github/workflows/build-next.yml index c7df7d05..dbc30af8 100644 --- a/.github/workflows/build-next.yml +++ b/.github/workflows/build-next.yml @@ -51,20 +51,20 @@ jobs: run: depot configure-docker - name: Login to GitHub Container Registry - uses: docker/login-action@v4.2.0 + uses: docker/login-action@v4.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub - uses: docker/login-action@v4.2.0 + uses: docker/login-action@v4.4.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v7.2.2 + uses: goreleaser/goreleaser-action@v7.2.3 with: distribution: goreleaser-pro version: "~> v2" diff --git a/.github/workflows/close-stale-feature-requests.yml b/.github/workflows/close-stale-feature-requests.yml deleted file mode 100644 index 7cfc0c57..00000000 --- a/.github/workflows/close-stale-feature-requests.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Close Stale Feature Requests - -on: - schedule: - - cron: "0 0 * * 1" # Runs every Monday at midnight - workflow_dispatch: - inputs: - dry-run: - description: "Log matching issues without closing them" - required: false - default: false - type: boolean - -permissions: - contents: read - issues: write - -jobs: - close-stale-feature-requests: - runs-on: ubuntu-latest - steps: - - uses: pocket-id/low-demand-closer@v1 - with: - dry-run: ${{ inputs['dry-run'] || false }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bc02cb52..3294e9ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,20 +46,20 @@ jobs: run: depot configure-docker - name: Login to GitHub Container Registry - uses: docker/login-action@v4.2.0 + uses: docker/login-action@v4.4.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Login to Docker Hub - uses: docker/login-action@v4.2.0 + uses: docker/login-action@v4.4.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v7.2.2 + uses: goreleaser/goreleaser-action@v7.2.3 with: distribution: goreleaser-pro version: "~> v2" diff --git a/.version b/.version index 10c2c0c3..46b81d81 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -2.10.0 +2.11.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5528006b..92c43dba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## v2.11.0 + +### Bug Fixes + +- postgres migration 20250822 remove public schema references ([#1582](https://github.com/pocket-id/pocket-id/pull/1582) by @MarcoScabbiolo) +- allow insecure callback URLs by default until next major release ([d9ead47](https://github.com/pocket-id/pocket-id/commit/d9ead47d19451b256336b7d0eb46606a236928f3) by @stonith404) +- sort signup tokens by creation date explicitly ([77398a5](https://github.com/pocket-id/pocket-id/commit/77398a558df45e1e332316afbb0f0c5e416350ea) by @stonith404) +- login code mobile ux ([#1584](https://github.com/pocket-id/pocket-id/pull/1584) by @James18232) +- `INTERNAL_APP_URL` not reflected in UI URLs ([316cf47](https://github.com/pocket-id/pocket-id/commit/316cf47cebdb7337cef13a533a8105ef4364b6f5) by @stonith404) +- ignore tab URL hashes in navigation history ([a3f27ec](https://github.com/pocket-id/pocket-id/commit/a3f27ec2ec1ce784ca11e5fb41f92a0367715393) by @stonith404) +- block link-local addresses in SSRF protection ([9714296](https://github.com/pocket-id/pocket-id/commit/9714296efb3768e4f29479f48628318565a3bed7) by @stonith404) + +### Documentation + +- update README ([ff5e565](https://github.com/pocket-id/pocket-id/commit/ff5e565eebbba0dba991cf06ad76249b7af26432) by @stonith404) + +### Features + +- add support for CIDR and IP address lists in `TRUST_PROXY` ([187cd8d](https://github.com/pocket-id/pocket-id/commit/187cd8ddcd4d6babec73bdf7e81eab2dee1ade04) by @stonith404) + +### Other + +- manage instance ID in the KV table ([#1579](https://github.com/pocket-id/pocket-id/pull/1579) by @ItalyPaleAle) +- Bump the "all-dependencies" group with 3 updates across multiple ecosystems ([#1578](https://github.com/pocket-id/pocket-id/pull/1578) by @dependabot[bot]) +- remove low demand issue closer ([bb03660](https://github.com/pocket-id/pocket-id/commit/bb03660bd7d48dfd0b022936dabd2b2880c9b382) by @stonith404) + +**Full Changelog**: https://github.com/pocket-id/pocket-id/compare/v2.10.0...v2.11.0 + ## v2.10.0 ### Bug Fixes diff --git a/backend/go.mod b/backend/go.mod index 7ba8435d..720dae61 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -4,9 +4,9 @@ go 1.26.5 require ( github.com/aws/aws-sdk-go-v2 v1.42.1 - github.com/aws/aws-sdk-go-v2/config v1.32.25 - github.com/aws/aws-sdk-go-v2/credentials v1.19.24 - github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 + github.com/aws/aws-sdk-go-v2/config v1.32.27 + github.com/aws/aws-sdk-go-v2/credentials v1.19.26 + github.com/aws/aws-sdk-go-v2/service/s3 v1.104.2 github.com/aws/smithy-go v1.27.3 github.com/caarlos0/env/v11 v11.4.1 github.com/cenkalti/backoff/v5 v5.0.3 @@ -38,7 +38,7 @@ require ( github.com/mileusna/useragent v1.3.5 github.com/orandin/slog-gorm v1.4.0 github.com/ory/fosite v0.49.1-0.20250703093431-a5f0b09bf31c - github.com/oschwald/maxminddb-golang/v2 v2.4.0 + github.com/oschwald/maxminddb-golang/v2 v2.4.1 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 github.com/zitadel/exifremove v0.1.0 @@ -54,10 +54,10 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 golang.org/x/crypto v0.53.0 golang.org/x/image v0.43.0 - golang.org/x/sync v0.21.0 - golang.org/x/text v0.38.0 + golang.org/x/sync v0.22.0 + golang.org/x/text v0.39.0 gorm.io/driver/postgres v1.6.0 - gorm.io/gorm v1.31.1 + gorm.io/gorm v1.31.2 gorm.io/plugin/opentelemetry v0.1.16 modernc.org/sqlite v1.53.0 ) @@ -70,15 +70,15 @@ require ( github.com/alphadose/haxmap v1.4.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.30 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.4.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.32.0 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.37.0 // indirect @@ -233,7 +233,7 @@ require ( golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.46.0 // indirect + golang.org/x/tools v0.47.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/grpc v1.82.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 9bc63bda..907e527f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -20,12 +20,12 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek= github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls= -github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM= -github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs= -github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I= -github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14/go.mod h1:zwM6veDkhGgQFqkBy+uT28AAYpLu+uFMlPl+rCg/73E= +github.com/aws/aws-sdk-go-v2/config v1.32.27 h1:SJwJ9Q4kM7v5QVSYYyXj3znRr6lNyZEhSgAXmXXcVbI= +github.com/aws/aws-sdk-go-v2/config v1.32.27/go.mod h1:uBfrzTRedDmB2u+b6+UlaKJy2O6VSH5un2jP24t/KvQ= +github.com/aws/aws-sdk-go-v2/credentials v1.19.26 h1:Si8kk1kyJnuJWCEgiwpBtTdtgSdR7i611596NnC0YIQ= +github.com/aws/aws-sdk-go-v2/credentials v1.19.26/go.mod h1:lBckz+W9SAdNtSDw3pYgQUJDJFcBBWry0GSzw+bK0TY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30 h1:/hi1JADLEW9YYryEz1w4GQu0EtP23pP553Cf9KgsDV4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.30/go.mod h1:/3AOgy4K17Dm4ucMZVC/MJkzy5kmfKUcINRHZyo0koQ= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.30 h1:xM/Is9cKMHa8Jj8zkvWhvrFkZsXJV9E+BB4g0HW0duQ= @@ -36,14 +36,14 @@ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31 h1:3GUprIsfmGcC5SACIyB0e7E0BM1 github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.31/go.mod h1:7PuV1yl5e2xnUbm+RqvVg5i2iBM8EyijZNoI9wsOoOc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23 h1:9Fjh6fi/U5JEStVZijmaMpUwE/gvBJj7x2B/PjbO9To= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.23/go.mod h1:iMoT2f1tClxrWAAnKCXjZQ6LOmfLrMG14wmnWpM+F14= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30 h1:/Z5jmNrKsSD7EmDjzAPsm/3L9IuOkzaynklJZ1qX7S4= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.30/go.mod h1:lEzEZnOosE7zi8Z6royW1cFJTD9fpab4Ul1SBrllewk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw= -github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0 h1:ta8csKy5vN91F3i5gGR85lFV0srBqySEji7Jroes6rE= -github.com/aws/aws-sdk-go-v2/service/s3 v1.104.0/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31 h1:uao4A3QZ5UmB326V6KF+qRpv9Tjz7IlnlnTbbANntlU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.31/go.mod h1:I/1+z0VwL1GhQyLgkoHDlygpUZ+iTAwOQ/NsftiUL2I= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.2 h1:bAY6O/TDv1HQnvylh9E247IyIKsUWUt2G965S7qX110= +github.com/aws/aws-sdk-go-v2/service/s3 v1.104.2/go.mod h1:zdmCoFO/dSI7GlrwsPqFJI+WlFnSU4Tc8TJnlXrM1Do= github.com/aws/aws-sdk-go-v2/service/signin v1.4.0 h1:sLzmJGCMv+C8KqiJgEqDLB6vxaJGmobRh4rr//ZpA3w= github.com/aws/aws-sdk-go-v2/service/signin v1.4.0/go.mod h1:mxC0nT/C8wMMS97DemZPzvUZxvIt+2Iq+eS3JdFZGgg= github.com/aws/aws-sdk-go-v2/service/sso v1.32.0 h1:qjMmry/cBDee1E/2gyvel0uRYCi3mwRZ2hf6N+GAodo= @@ -412,8 +412,8 @@ github.com/ory/pop/v6 v6.4.1 h1:mxwfgwIB+kRlE4hvcoeEuxFqXZai6TWgQ23sOCBTERo= github.com/ory/pop/v6 v6.4.1/go.mod h1:o+a3+gdnfWUd/IpFCTKidX7sRgQ6GdPmH02XYiMH8cw= github.com/ory/x v0.0.729 h1:7ttCYNCjCdspI6X0oaxGAXoiYWSBrwGRz6w/IG8s3I4= github.com/ory/x v0.0.729/go.mod h1:qdUK3Sp4K4nRbYJG0sEnFO1tDLN/Ct53G+ymre0JhCU= -github.com/oschwald/maxminddb-golang/v2 v2.4.0 h1:3ftnrR1/XwiQ788bWIRhsE1DK3GOgJ6tm6S2qTktLm8= -github.com/oschwald/maxminddb-golang/v2 v2.4.0/go.mod h1:7jcFtmhWVDEV+UopVv9NjcPm200uMyEHN14LIVV4hW8= +github.com/oschwald/maxminddb-golang/v2 v2.4.1 h1:OffzqSABE3Sw354GdBThqDsKfpA4GWBqOY2P91V8tjI= +github.com/oschwald/maxminddb-golang/v2 v2.4.1/go.mod h1:CZK8iQQMKfy6mKOifoyUmrj4vTHnMiGVaS7hDaZZxQ0= github.com/paulmach/orb v0.11.1 h1:3koVegMC4X/WeiXYz9iswopaTwMem53NzTJuTF20JzU= github.com/paulmach/orb v0.11.1/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= @@ -670,8 +670,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= -golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -711,8 +711,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -724,8 +724,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk= -golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -763,8 +763,8 @@ gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXD gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= -gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= +gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/plugin/opentelemetry v0.1.16 h1:Kypj2YYAliJqkIczDZDde6P6sFMhKSlG5IpngMFQGpc= gorm.io/plugin/opentelemetry v0.1.16/go.mod h1:P3RmTeZXT+9n0F1ccUqR5uuTvEXDxF8k2UpO7mTIB2Y= k8s.io/utils v0.0.0-20260617174310-a95e086a2553 h1:hmGqDecjc8d7HVzWzRFl0QD9bYuYKbBEG7t8xwnVxfI= diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index b92f3ecc..3b3015a0 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -70,7 +70,10 @@ func initEngine() (*gin.Engine, error) { r := gin.New() initLogger(r) - configureEngine(r) + err := configureEngine(r) + if err != nil { + return nil, err + } registerGlobalMiddleware(r) return r, nil @@ -88,9 +91,10 @@ func setGinMode() { } } -func configureEngine(r *gin.Engine) { - if !common.EnvConfig.TrustProxy { - _ = r.SetTrustedProxies(nil) +func configureEngine(r *gin.Engine) error { + err := r.SetTrustedProxies(common.EnvConfig.TrustProxy) + if err != nil { + return fmt.Errorf("failed to configure trusted proxies: %w", err) } if common.EnvConfig.TrustedPlatform != "" { @@ -101,6 +105,8 @@ func configureEngine(r *gin.Engine) { common.Name, otelgin.WithFilter(shouldTraceRequest)), ) + + return nil } // shouldTraceRequest reports whether an incoming request should be traced. diff --git a/backend/internal/bootstrap/services_bootstrap.go b/backend/internal/bootstrap/services_bootstrap.go index cfc8ac8d..d05edf06 100644 --- a/backend/internal/bootstrap/services_bootstrap.go +++ b/backend/internal/bootstrap/services_bootstrap.go @@ -88,9 +88,10 @@ func initServices(ctx context.Context, db *gorm.DB, instanceID string, httpClien DB: db, HTTPClient: httpClient, Config: oidc.Config{ - BaseURL: common.EnvConfig.AppURL, - TokenBaseURL: common.EnvConfig.AppURL, - Secret: common.EnvConfig.EncryptionKey, + BaseURL: common.EnvConfig.AppURL, + TokenBaseURL: common.EnvConfig.AppURL, + Secret: common.EnvConfig.EncryptionKey, + AllowInsecureCallbackURLs: common.EnvConfig.AllowInsecureCallbackURLs, }, Signer: svc.jwtService, CustomClaims: svc.customClaimService, diff --git a/backend/internal/common/env_config.go b/backend/internal/common/env_config.go index a98738fc..601f92da 100644 --- a/backend/internal/common/env_config.go +++ b/backend/internal/common/env_config.go @@ -8,6 +8,7 @@ import ( "net/url" "os" "reflect" + "strconv" "strings" "github.com/caarlos0/env/v11" @@ -17,6 +18,7 @@ import ( type AppEnv string type DbProvider string +type TrustProxyConfig []string const ( // TracerName should be passed to otel.Tracer, trace.SpanFromContext when creating custom spans. @@ -38,21 +40,22 @@ const ( ) type EnvConfigSchema struct { - AppEnv AppEnv `env:"APP_ENV" options:"toLower"` - EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"` - AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"` - DbProvider DbProvider - DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"` - TrustProxy bool `env:"TRUST_PROXY"` - TrustedPlatform string `env:"TRUSTED_PLATFORM"` - AuditLogRetentionDays int `env:"AUDIT_LOG_RETENTION_DAYS"` - AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"` - AllowDowngrade bool `env:"ALLOW_DOWNGRADE"` - InternalAppURL string `env:"INTERNAL_APP_URL"` - UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"` - DisableRateLimiting bool `env:"DISABLE_RATE_LIMITING"` - VersionCheckDisabled bool `env:"VERSION_CHECK_DISABLED"` - StaticApiKey string `env:"STATIC_API_KEY" options:"file"` + AppEnv AppEnv `env:"APP_ENV" options:"toLower"` + EncryptionKey []byte `env:"ENCRYPTION_KEY" options:"file"` + AppURL string `env:"APP_URL" options:"toLower,trimTrailingSlash"` + DbProvider DbProvider + DbConnectionString string `env:"DB_CONNECTION_STRING" options:"file"` + TrustProxy TrustProxyConfig `env:"TRUST_PROXY"` + TrustedPlatform string `env:"TRUSTED_PLATFORM"` + AuditLogRetentionDays int `env:"AUDIT_LOG_RETENTION_DAYS"` + AnalyticsDisabled bool `env:"ANALYTICS_DISABLED"` + AllowDowngrade bool `env:"ALLOW_DOWNGRADE"` + AllowInsecureCallbackURLs bool `env:"ALLOW_INSECURE_CALLBACK_URLS"` + InternalAppURL string `env:"INTERNAL_APP_URL"` + UiConfigDisabled bool `env:"UI_CONFIG_DISABLED"` + DisableRateLimiting bool `env:"DISABLE_RATE_LIMITING"` + VersionCheckDisabled bool `env:"VERSION_CHECK_DISABLED"` + StaticApiKey string `env:"STATIC_API_KEY" options:"file"` FileBackend string `env:"FILE_BACKEND" options:"toLower"` UploadPath string `env:"UPLOAD_PATH"` @@ -97,18 +100,19 @@ func init() { func defaultConfig() EnvConfigSchema { return EnvConfigSchema{ - AppEnv: AppEnvProduction, - LogLevel: "info", - DbProvider: "sqlite", - FileBackend: "filesystem", - AuditLogRetentionDays: 90, - AppURL: AppUrl, - Port: "1411", - Host: "0.0.0.0", - ActorsPort: "1414", - ActorsHost: "0.0.0.0", - GeoLiteDBPath: "data/GeoLite2-City.mmdb", - GeoLiteDBUrl: MaxMindGeoLiteCityUrl, + AppEnv: AppEnvProduction, + LogLevel: "info", + DbProvider: "sqlite", + FileBackend: "filesystem", + AuditLogRetentionDays: 90, + AllowInsecureCallbackURLs: true, // TODO: Default to false in major v3 + AppURL: AppUrl, + Port: "1411", + Host: "0.0.0.0", + ActorsPort: "1414", + ActorsHost: "0.0.0.0", + GeoLiteDBPath: "data/GeoLite2-City.mmdb", + GeoLiteDBUrl: MaxMindGeoLiteCityUrl, } } @@ -390,3 +394,29 @@ func (a AppEnv) IsProduction() bool { func (a AppEnv) IsTest() bool { return a == AppEnvTest } + +func (config *TrustProxyConfig) UnmarshalText(text []byte) error { + value := strings.TrimSpace(string(text)) + + // Support boolean values for completely enabling or disabling trust proxy + enabled, err := strconv.ParseBool(value) + if err == nil { + if enabled { + *config = TrustProxyConfig{"0.0.0.0/0", "::/0"} + } else { + *config = nil + } + + return nil + } + + // Normalize and validate each explicit proxy before the server starts + proxies := strings.Split(value, ",") + for i, proxy := range proxies { + proxy = strings.TrimSpace(proxy) + proxies[i] = proxy + } + + *config = proxies + return nil +} diff --git a/backend/internal/common/env_config_test.go b/backend/internal/common/env_config_test.go index a64ae75d..2eabdbcc 100644 --- a/backend/internal/common/env_config_test.go +++ b/backend/internal/common/env_config_test.go @@ -119,12 +119,36 @@ func TestParseEnvConfig(t *testing.T) { t.Setenv("TRACING_ENABLED", "false") t.Setenv("TRUST_PROXY", "true") t.Setenv("ANALYTICS_DISABLED", "false") + t.Setenv("ALLOW_INSECURE_CALLBACK_URLS", "false") err := parseAndValidateEnvConfig(t) require.NoError(t, err) assert.True(t, EnvConfig.UiConfigDisabled) - assert.True(t, EnvConfig.TrustProxy) + assert.Equal(t, TrustProxyConfig{"0.0.0.0/0", "::/0"}, EnvConfig.TrustProxy) assert.False(t, EnvConfig.AnalyticsDisabled) + assert.False(t, EnvConfig.AllowInsecureCallbackURLs) + }) + + t.Run("should parse trusted proxy IP addresses and CIDR ranges", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("TRUST_PROXY", "10.0.0.0/8, 192.168.1.10, ::1/128") + + err := parseAndValidateEnvConfig(t) + require.NoError(t, err) + assert.Equal(t, TrustProxyConfig{"10.0.0.0/8", "192.168.1.10", "::1/128"}, EnvConfig.TrustProxy) + }) + + t.Run("should disable trusted proxies when set to false", func(t *testing.T) { + EnvConfig = defaultConfig() + t.Setenv("TRUST_PROXY", "false") + + err := parseAndValidateEnvConfig(t) + require.NoError(t, err) + assert.Nil(t, EnvConfig.TrustProxy) + }) + + t.Run("should allow insecure callback URLs by default", func(t *testing.T) { + assert.True(t, defaultConfig().AllowInsecureCallbackURLs) }) t.Run("should default audit log retention days to 90", func(t *testing.T) { diff --git a/backend/internal/oidc/module.go b/backend/internal/oidc/module.go index 3e6755c1..6ff546ca 100644 --- a/backend/internal/oidc/module.go +++ b/backend/internal/oidc/module.go @@ -15,9 +15,10 @@ import ( ) type Config struct { - BaseURL string - TokenBaseURL string - Secret []byte + BaseURL string + TokenBaseURL string + Secret []byte + AllowInsecureCallbackURLs bool } type TokenSigner interface { diff --git a/backend/internal/oidc/provider.go b/backend/internal/oidc/provider.go index 76827bfb..0604700d 100644 --- a/backend/internal/oidc/provider.go +++ b/backend/internal/oidc/provider.go @@ -49,6 +49,7 @@ func newProvider(store *Store, authenticator *federatedClientAuthenticator, sign IgnoreUnknownScopes: true, AudienceMatchingStrategy: fosite.ExactAudienceMatchingStrategy, RedirectURIMatcher: matchRedirectURI, + RedirectSecureChecker: redirectSecureChecker(config.AllowInsecureCallbackURLs), EnforcePKCEForPublicClients: true, EnablePKCEPlainChallengeMethod: true, SupportedRequestObjectSigningAlgorithms: []string{"none"}, @@ -111,6 +112,17 @@ func newProvider(store *Store, authenticator *federatedClientAuthenticator, sign }, nil } +func redirectSecureChecker(allowInsecureCallbackURLs bool) func(context.Context, *url.URL) bool { + return func(ctx context.Context, redirectURI *url.URL) bool { + if allowInsecureCallbackURLs || fosite.IsRedirectURISecure(ctx, redirectURI) { + return true + } + + slog.InfoContext(ctx, "HTTP callback URL rejected; set ALLOW_INSECURE_CALLBACK_URLS=true to allow it", "callback_url", redirectURI.Redacted()) + return false + } +} + func matchRedirectURI(rawurl string, client fosite.Client) (*url.URL, error) { redirectURI, err := fosite.MatchRedirectURIWithClientRedirectURIs(rawurl, client) if err == nil || rawurl == "" { diff --git a/backend/internal/oidc/provider_test.go b/backend/internal/oidc/provider_test.go index 6f25a3e7..e60c6279 100644 --- a/backend/internal/oidc/provider_test.go +++ b/backend/internal/oidc/provider_test.go @@ -11,6 +11,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "net/url" "strings" "testing" "time" @@ -85,6 +86,70 @@ func TestProviderIssuesJWTAccessTokens(t *testing.T) { require.Equal(t, "test-key-id", header["kid"]) } +func TestRedirectSecureChecker(t *testing.T) { + loopbackRedirectURI, err := url.Parse("http://127.0.0.1:49813/callback") + require.NoError(t, err) + + checker := redirectSecureChecker(false) + require.True(t, checker(t.Context(), loopbackRedirectURI)) +} + +func TestProviderInsecureCallbackURLCompatibility(t *testing.T) { + tests := []struct { + name string + allowInsecureCallbackURLs bool + expectSuccess bool + }{ + { + name: "allows HTTP callback URLs when compatibility is enabled", + allowInsecureCallbackURLs: true, + expectSuccess: true, + }, + { + name: "rejects HTTP callback URLs when compatibility is disabled", + allowInsecureCallbackURLs: false, + expectSuccess: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + signerKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + require.NoError(t, db.Create(&model.OidcClient{ + Base: model.Base{ID: "test-client"}, + Name: "Test Client", + CallbackURLs: model.UrlList{"http://client.example.com/callback"}, + }).Error) + + provider, err := newProvider(NewStore(db, nil), nil, testTokenSigner{key: signerKey}, Config{ //nolint:gosec // static test-only provider secret + BaseURL: "https://issuer.example.com", + TokenBaseURL: "https://issuer.example.com", + Secret: []byte("test-secret"), + AllowInsecureCallbackURLs: tt.allowInsecureCallbackURLs, + }) + require.NoError(t, err) + + req := httptest.NewRequestWithContext( + t.Context(), + http.MethodGet, + "/api/oidc/authorize?client_id=test-client&response_type=code&scope=openid&state=state-with-enough-entropy&redirect_uri=http://client.example.com/callback", + nil, + ) + authorizeRequest, err := provider.NewAuthorizeRequest(req.Context(), req) + require.NoError(t, err) + + _, err = provider.NewAuthorizeResponse(t.Context(), authorizeRequest, NewEmptySession()) + if tt.expectSuccess { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, fosite.ErrInvalidRequest) + } + }) + } +} + func TestProviderAcceptsWildcardRedirectURI(t *testing.T) { db := testutils.NewDatabaseForTest(t) signerKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) diff --git a/backend/internal/utils/ip_util.go b/backend/internal/utils/ip_util.go index 4136658f..d91850ad 100644 --- a/backend/internal/utils/ip_util.go +++ b/backend/internal/utils/ip_util.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net" + "net/netip" "net/url" "strings" @@ -56,7 +57,17 @@ func IsTailscaleIP(ip net.IP) bool { } func IsPrivateIP(ip net.IP) bool { - return IsLocalhostIP(ip) || IsPrivateLanIP(ip) || IsTailscaleIP(ip) || IsLocalIPv6(ip) + if IsLocalhostIP(ip) || IsPrivateLanIP(ip) || IsTailscaleIP(ip) || IsLocalIPv6(ip) { + return true + } + + addr, ok := netip.AddrFromSlice(ip) + if !ok { + return false + } + + addr = addr.Unmap() + return addr.IsLoopback() || addr.IsPrivate() || addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast() || addr.IsUnspecified() } func IsURLPrivate(ctx context.Context, u *url.URL) (bool, error) { diff --git a/backend/internal/utils/ip_util_test.go b/backend/internal/utils/ip_util_test.go index 5da1eb68..05d52a6f 100644 --- a/backend/internal/utils/ip_util_test.go +++ b/backend/internal/utils/ip_util_test.go @@ -110,12 +110,19 @@ func TestIsPrivateIP(t *testing.T) { ip string expected bool }{ - {"127.0.0.1", true}, // localhost - {"192.168.1.1", true}, // private LAN - {"100.64.0.1", true}, // Tailscale - {"fd00::1", true}, // local IPv6 - {"8.8.8.8", false}, // public IPv4 - {"2001:4860:4860::8888", false}, // public IPv6 + {"127.0.0.1", true}, // localhost + {"192.168.1.1", true}, // private LAN + {"100.64.0.1", true}, // Tailscale + {"169.254.169.254", true}, // IPv4 link-local + {"169.254.170.2", true}, // IPv4 link-local + {"::ffff:169.254.169.254", true}, // IPv4-mapped link-local + {"fe80::1", true}, // IPv6 link-local + {"ff02::1", true}, // IPv6 link-local multicast + {"0.0.0.0", true}, // IPv4 unspecified + {"::", true}, // IPv6 unspecified + {"fd00::1", true}, // private IPv6 + {"8.8.8.8", false}, // public IPv4 + {"2001:4860:4860::8888", false}, // public IPv6 } for _, tt := range tests { @@ -231,6 +238,36 @@ func TestIsURLPrivate(t *testing.T) { expectPriv: true, expectError: false, }, + { + name: "IPv4 link-local metadata IP", + urlStr: "http://169.254.169.254", + expectPriv: true, + expectError: false, + }, + { + name: "IPv4-mapped link-local metadata IP", + urlStr: "http://[::ffff:169.254.169.254]", + expectPriv: true, + expectError: false, + }, + { + name: "IPv6 link-local IP", + urlStr: "http://[fe80::1]", + expectPriv: true, + expectError: false, + }, + { + name: "IPv4 unspecified IP", + urlStr: "http://0.0.0.0", + expectPriv: true, + expectError: false, + }, + { + name: "IPv6 unspecified IP", + urlStr: "http://[::]", + expectPriv: true, + expectError: false, + }, { name: "public IP - Google DNS", urlStr: "http://8.8.8.8", diff --git a/email-templates/package.json b/email-templates/package.json index 26cbc9ef..80b1fd02 100644 --- a/email-templates/package.json +++ b/email-templates/package.json @@ -19,7 +19,7 @@ "@types/node": "^25.9.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "react-email": "6.6.5", + "react-email": "6.6.6", "tsx": "^4.22.2" } } diff --git a/frontend/package.json b/frontend/package.json index 9d066959..a42b77ca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "pocket-id-frontend", - "version": "2.10.0", + "version": "2.11.0", "private": true, "type": "module", "scripts": { @@ -15,7 +15,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", "@opentelemetry/resources": "^2.8.0", "@opentelemetry/sdk-trace-web": "^2.8.0", "@opentelemetry/semantic-conventions": "^1.41.1", diff --git a/frontend/src/lib/components/login-wrapper.svelte b/frontend/src/lib/components/login-wrapper.svelte index 04ee7bfe..41ddc39e 100644 --- a/frontend/src/lib/components/login-wrapper.svelte +++ b/frontend/src/lib/components/login-wrapper.svelte @@ -106,7 +106,7 @@ {:else}