Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package regclient for openSUSE:Factory checked in at 2026-05-29 18:08:50 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/regclient (Old) and /work/SRC/openSUSE:Factory/.regclient.new.1937 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "regclient" Fri May 29 18:08:50 2026 rev:16 rq:1355795 version:0.11.5 Changes: -------- --- /work/SRC/openSUSE:Factory/regclient/regclient.changes 2026-05-12 19:29:06.925172370 +0200 +++ /work/SRC/openSUSE:Factory/.regclient.new.1937/regclient.changes 2026-05-29 18:10:40.547298393 +0200 @@ -1,0 +2,11 @@ +Fri May 29 07:04:36 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.11.5: + * Security: + - Prevent https to non-https downgrades and localhost + redirects. (PR 1093) + - Forbid sending auth on redirects. (PR 1095) + * Features: + - Add regbot manifest.descriptor to the sandbox. (PR 1091) + +------------------------------------------------------------------- Old: ---- regclient-0.11.4.obscpio New: ---- regclient-0.11.5.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ regclient.spec ++++++ --- /var/tmp/diff_new_pack.A4VFbs/_old 2026-05-29 18:10:41.891356234 +0200 +++ /var/tmp/diff_new_pack.A4VFbs/_new 2026-05-29 18:10:41.895356406 +0200 @@ -17,7 +17,7 @@ Name: regclient -Version: 0.11.4 +Version: 0.11.5 Release: 0 Summary: OCI Registry Client in Go and tooling using those libraries License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.A4VFbs/_old 2026-05-29 18:10:41.931357956 +0200 +++ /var/tmp/diff_new_pack.A4VFbs/_new 2026-05-29 18:10:41.943358472 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/regclient/regclient</param> <param name="scm">git</param> <param name="package-meta">yes</param> - <param name="revision">v0.11.4</param> + <param name="revision">v0.11.5</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.A4VFbs/_old 2026-05-29 18:10:41.975359849 +0200 +++ /var/tmp/diff_new_pack.A4VFbs/_new 2026-05-29 18:10:41.979360021 +0200 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/regclient/regclient</param> - <param name="changesrevision">8b080b448b5f7dac833fd14e5e9979d96e164164</param></service></servicedata> + <param name="changesrevision">2bc542b4a19d6e4fd939be150f1443da2395690c</param></service></servicedata> (No newline at EOF) ++++++ regclient-0.11.4.obscpio -> regclient-0.11.5.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.git/HEAD new/regclient-0.11.5/.git/HEAD --- old/regclient-0.11.4/.git/HEAD 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.git/HEAD 2026-05-26 15:04:56.000000000 +0200 @@ -1 +1 @@ -8b080b448b5f7dac833fd14e5e9979d96e164164 +2bc542b4a19d6e4fd939be150f1443da2395690c diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.git/ORIG_HEAD new/regclient-0.11.5/.git/ORIG_HEAD --- old/regclient-0.11.4/.git/ORIG_HEAD 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.git/ORIG_HEAD 2026-05-26 15:04:56.000000000 +0200 @@ -1 +1 @@ -8b080b448b5f7dac833fd14e5e9979d96e164164 +2bc542b4a19d6e4fd939be150f1443da2395690c Binary files old/regclient-0.11.4/.git/index and new/regclient-0.11.5/.git/index differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.git/logs/HEAD new/regclient-0.11.5/.git/logs/HEAD --- old/regclient-0.11.4/.git/logs/HEAD 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.git/logs/HEAD 2026-05-26 15:04:56.000000000 +0200 @@ -1,2 +1,2 @@ -0000000000000000000000000000000000000000 b413945eb6f65d26be7cbbda4ddba6249e823a8b kastl <[email protected]> 1778562145 +0200 clone: from https://github.com/regclient/regclient -b413945eb6f65d26be7cbbda4ddba6249e823a8b 8b080b448b5f7dac833fd14e5e9979d96e164164 kastl <[email protected]> 1778562145 +0200 checkout: moving from main to v0.11.4 +0000000000000000000000000000000000000000 bb093cfc9adb7f2926825370a158f37e86998cae Johannes Kastl <[email protected]> 1780038275 +0200 clone: from https://github.com/regclient/regclient +bb093cfc9adb7f2926825370a158f37e86998cae 2bc542b4a19d6e4fd939be150f1443da2395690c Johannes Kastl <[email protected]> 1780038275 +0200 checkout: moving from main to v0.11.5 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.git/logs/refs/heads/main new/regclient-0.11.5/.git/logs/refs/heads/main --- old/regclient-0.11.4/.git/logs/refs/heads/main 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.git/logs/refs/heads/main 2026-05-26 15:04:56.000000000 +0200 @@ -1 +1 @@ -0000000000000000000000000000000000000000 b413945eb6f65d26be7cbbda4ddba6249e823a8b kastl <[email protected]> 1778562145 +0200 clone: from https://github.com/regclient/regclient +0000000000000000000000000000000000000000 bb093cfc9adb7f2926825370a158f37e86998cae Johannes Kastl <[email protected]> 1780038275 +0200 clone: from https://github.com/regclient/regclient diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.git/logs/refs/remotes/origin/HEAD new/regclient-0.11.5/.git/logs/refs/remotes/origin/HEAD --- old/regclient-0.11.4/.git/logs/refs/remotes/origin/HEAD 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.git/logs/refs/remotes/origin/HEAD 2026-05-26 15:04:56.000000000 +0200 @@ -1 +1 @@ -0000000000000000000000000000000000000000 b413945eb6f65d26be7cbbda4ddba6249e823a8b kastl <[email protected]> 1778562145 +0200 clone: from https://github.com/regclient/regclient +0000000000000000000000000000000000000000 bb093cfc9adb7f2926825370a158f37e86998cae Johannes Kastl <[email protected]> 1780038275 +0200 clone: from https://github.com/regclient/regclient Binary files old/regclient-0.11.4/.git/objects/pack/pack-5376dd2e834d247883883347763f313f79e95fc0.idx and new/regclient-0.11.5/.git/objects/pack/pack-5376dd2e834d247883883347763f313f79e95fc0.idx differ Binary files old/regclient-0.11.4/.git/objects/pack/pack-5376dd2e834d247883883347763f313f79e95fc0.pack and new/regclient-0.11.5/.git/objects/pack/pack-5376dd2e834d247883883347763f313f79e95fc0.pack differ Binary files old/regclient-0.11.4/.git/objects/pack/pack-5376dd2e834d247883883347763f313f79e95fc0.rev and new/regclient-0.11.5/.git/objects/pack/pack-5376dd2e834d247883883347763f313f79e95fc0.rev differ Binary files old/regclient-0.11.4/.git/objects/pack/pack-809f8341d0996b128fff4e8a7b470964b26c625b.idx and new/regclient-0.11.5/.git/objects/pack/pack-809f8341d0996b128fff4e8a7b470964b26c625b.idx differ Binary files old/regclient-0.11.4/.git/objects/pack/pack-809f8341d0996b128fff4e8a7b470964b26c625b.pack and new/regclient-0.11.5/.git/objects/pack/pack-809f8341d0996b128fff4e8a7b470964b26c625b.pack differ Binary files old/regclient-0.11.4/.git/objects/pack/pack-809f8341d0996b128fff4e8a7b470964b26c625b.rev and new/regclient-0.11.5/.git/objects/pack/pack-809f8341d0996b128fff4e8a7b470964b26c625b.rev differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.git/packed-refs new/regclient-0.11.5/.git/packed-refs --- old/regclient-0.11.4/.git/packed-refs 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.git/packed-refs 2026-05-26 15:04:56.000000000 +0200 @@ -1,8 +1,8 @@ # pack-refs with: peeled fully-peeled sorted -b413945eb6f65d26be7cbbda4ddba6249e823a8b refs/remotes/origin/main +bb093cfc9adb7f2926825370a158f37e86998cae refs/remotes/origin/main daa734a0b4dc9c19231cfe691a241f0ce2a7b2f4 refs/remotes/origin/releases/0.0 c3de9bb7b04ad3cd96bb73bc0c72986b83572d12 refs/remotes/origin/releases/0.10 -8b080b448b5f7dac833fd14e5e9979d96e164164 refs/remotes/origin/releases/0.11 +2bc542b4a19d6e4fd939be150f1443da2395690c refs/remotes/origin/releases/0.11 4c6dd972a3c609f7c0997bb6e464aee431f8c971 refs/remotes/origin/releases/0.2 6a1a13c410f734f5e18a6032936bc6764814eae7 refs/remotes/origin/releases/0.3 847254c7ac7d6f027dcdfb196a9aa4c11eb61ed9 refs/remotes/origin/releases/0.4 @@ -35,6 +35,8 @@ ^1736a5711c8d6f71b40a279673c0023077709049 2f60255614ef0bc9f850e291dc921987163a8027 refs/tags/v0.11.4 ^8b080b448b5f7dac833fd14e5e9979d96e164164 +c05b11580971eba092b9844c5eb32660d64f5c9f refs/tags/v0.11.5 +^2bc542b4a19d6e4fd939be150f1443da2395690c f5e39881d000960a706d1840f2a43eac7b3fd9de refs/tags/v0.2.0 ^5906ef88ec6ec3f6709d286756d27b77982fa55b 25bacba961cde26845ce0bf5a90edbb8a2bdb2e4 refs/tags/v0.2.1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.git/refs/heads/main new/regclient-0.11.5/.git/refs/heads/main --- old/regclient-0.11.4/.git/refs/heads/main 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.git/refs/heads/main 2026-05-26 15:04:56.000000000 +0200 @@ -1 +1 @@ -b413945eb6f65d26be7cbbda4ddba6249e823a8b +bb093cfc9adb7f2926825370a158f37e86998cae diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.github/workflows/docker.yml new/regclient-0.11.5/.github/workflows/docker.yml --- old/regclient-0.11.4/.github/workflows/docker.yml 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.github/workflows/docker.yml 2026-05-26 15:04:56.000000000 +0200 @@ -87,25 +87,25 @@ echo "repo_url=${REPO_URL}" >>$GITHUB_OUTPUT - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Login to DockerHub if: github.repository_owner == 'regclient' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GHCR if: github.repository_owner == 'regclient' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ghcr.io username: ${{ secrets.GHCR_USERNAME }} password: ${{ secrets.GHCR_TOKEN }} - name: Build - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 id: build with: context: . @@ -135,7 +135,7 @@ # Dogfooding, use regctl to modify regclient images to improve reproducibility - name: Install regctl - uses: regclient/actions/regctl-installer@f3c6d87835906c175eb6ccfc18b348b69bb447e7 # main + uses: regclient/actions/regctl-installer@c70ad64367908075211b10dcd2ab9fad4bfa1816 # main if: github.event_name != 'pull_request' && github.repository_owner == 'regclient' with: release: main diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.github/workflows/inactive-cleanup.yml new/regclient-0.11.5/.github/workflows/inactive-cleanup.yml --- old/regclient-0.11.4/.github/workflows/inactive-cleanup.yml 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.github/workflows/inactive-cleanup.yml 2026-05-26 15:04:56.000000000 +0200 @@ -13,7 +13,7 @@ issues: write # for actions/stale to close stale issues pull-requests: write # for actions/stale to close stale PRs steps: - - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 + - uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 with: days-before-stale: 60 days-before-close: 28 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/.version-bump.lock new/regclient-0.11.5/.version-bump.lock --- old/regclient-0.11.4/.version-bump.lock 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/.version-bump.lock 2026-05-26 15:04:56.000000000 +0200 @@ -15,28 +15,28 @@ {"name":"gha-syft-version","key":"docker.io/anchore/syft","version":"v1.44.0"} {"name":"gha-uses-commit","key":"https://github.com/actions/checkout.git:v6.0.2","version":"de0fac2e4500dabe0009e67214ff5f5447ce83dd"} {"name":"gha-uses-commit","key":"https://github.com/actions/setup-go.git:v6.4.0","version":"4a3601121dd01d1626a1e23e37211e3254c1c06c"} -{"name":"gha-uses-commit","key":"https://github.com/actions/stale.git:v10.2.0","version":"b5d41d4e1d5dceea10e7104786b73624c18a190f"} +{"name":"gha-uses-commit","key":"https://github.com/actions/stale.git:v10.3.0","version":"eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899"} {"name":"gha-uses-commit","key":"https://github.com/actions/upload-artifact.git:v7.0.1","version":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"} {"name":"gha-uses-commit","key":"https://github.com/anchore/sbom-action.git:v0.24.0","version":"e22c389904149dbc22b58101806040fa8d37a610"} -{"name":"gha-uses-commit","key":"https://github.com/docker/build-push-action.git:v7.1.0","version":"bcafcacb16a39f128d818304e6c9c0c18556b85f"} -{"name":"gha-uses-commit","key":"https://github.com/docker/login-action.git:v4.1.0","version":"4907a6ddec9925e35a0a9e82d7399ccc52663121"} -{"name":"gha-uses-commit","key":"https://github.com/docker/setup-buildx-action.git:v4.0.0","version":"4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd"} -{"name":"gha-uses-commit","key":"https://github.com/regclient/actions.git:main","version":"f3c6d87835906c175eb6ccfc18b348b69bb447e7"} +{"name":"gha-uses-commit","key":"https://github.com/docker/build-push-action.git:v7.2.0","version":"f9f3042f7e2789586610d6e8b85c8f03e5195baf"} +{"name":"gha-uses-commit","key":"https://github.com/docker/login-action.git:v4.2.0","version":"650006c6eb7dba73a995cc03b0b2d7f5ca915bee"} +{"name":"gha-uses-commit","key":"https://github.com/docker/setup-buildx-action.git:v4.1.0","version":"d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5"} +{"name":"gha-uses-commit","key":"https://github.com/regclient/actions.git:main","version":"c70ad64367908075211b10dcd2ab9fad4bfa1816"} {"name":"gha-uses-commit","key":"https://github.com/sigstore/cosign-installer.git:v4.1.2","version":"6f9f17788090df1f26f669e9d70d6ae9567deba6"} {"name":"gha-uses-commit","key":"https://github.com/softprops/action-gh-release.git:v3.0.0","version":"b4309332981a82ec1c5618f44dd2e27cc8bfbfda"} {"name":"gha-uses-semver","key":"https://github.com/actions/checkout.git","version":"v6.0.2"} {"name":"gha-uses-semver","key":"https://github.com/actions/setup-go.git","version":"v6.4.0"} -{"name":"gha-uses-semver","key":"https://github.com/actions/stale.git","version":"v10.2.0"} +{"name":"gha-uses-semver","key":"https://github.com/actions/stale.git","version":"v10.3.0"} {"name":"gha-uses-semver","key":"https://github.com/actions/upload-artifact.git","version":"v7.0.1"} {"name":"gha-uses-semver","key":"https://github.com/anchore/sbom-action.git","version":"v0.24.0"} -{"name":"gha-uses-semver","key":"https://github.com/docker/build-push-action.git","version":"v7.1.0"} -{"name":"gha-uses-semver","key":"https://github.com/docker/login-action.git","version":"v4.1.0"} -{"name":"gha-uses-semver","key":"https://github.com/docker/setup-buildx-action.git","version":"v4.0.0"} +{"name":"gha-uses-semver","key":"https://github.com/docker/build-push-action.git","version":"v7.2.0"} +{"name":"gha-uses-semver","key":"https://github.com/docker/login-action.git","version":"v4.2.0"} +{"name":"gha-uses-semver","key":"https://github.com/docker/setup-buildx-action.git","version":"v4.1.0"} {"name":"gha-uses-semver","key":"https://github.com/sigstore/cosign-installer.git","version":"v4.1.2"} {"name":"gha-uses-semver","key":"https://github.com/softprops/action-gh-release.git","version":"v3.0.0"} {"name":"go-mod-golang-release","key":"golang-oldest","version":"1.25.0"} {"name":"makefile-ci-distribution","key":"docker.io/library/registry","version":"3.1.1"} -{"name":"makefile-ci-zot","key":"ghcr.io/project-zot/zot-linux-amd64","version":"v2.1.16"} +{"name":"makefile-ci-zot","key":"ghcr.io/project-zot/zot-linux-amd64","version":"v2.1.17"} {"name":"makefile-go-vulncheck","key":"https://go.googlesource.com/vuln.git","version":"v1.3.0"} {"name":"makefile-gofumpt","key":"https://github.com/mvdan/gofumpt.git","version":"v0.10.0"} {"name":"makefile-gomajor","key":"https://github.com/icholy/gomajor.git","version":"v0.15.0"} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/Makefile new/regclient-0.11.5/Makefile --- old/regclient-0.11.4/Makefile 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/Makefile 2026-05-26 15:04:56.000000000 +0200 @@ -53,7 +53,7 @@ endif STATICCHECK_VER?=v0.7.0 CI_DISTRIBUTION_VER?=3.1.1 -CI_ZOT_VER?=v2.1.16 +CI_ZOT_VER?=v2.1.17 .PHONY: .FORCE .FORCE: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/cmd/regbot/sandbox/blob.go new/regclient-0.11.5/cmd/regbot/sandbox/blob.go --- old/regclient-0.11.4/cmd/regbot/sandbox/blob.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/cmd/regbot/sandbox/blob.go 2026-05-26 15:04:56.000000000 +0200 @@ -45,59 +45,6 @@ ) } -// func (s *Sandbox) checkBlob(ls *lua.LState, i int, head bool) *sbBlob { -// var b *sbBlob -// switch ls.Get(i).Type() { -// case lua.LTString: -// r, err := ref.New(ls.CheckString(1)) -// if err != nil { -// ls.RaiseError("reference parsing failed: %v", err) -// } -// if head { -// rcB, err := s.rc.BlobHead(s.ctx, r, digest.Digest(r.Digest)) -// if err != nil { -// ls.RaiseError("Failed retrieving \"%s\" blob: %v", r.CommonName(), err) -// } -// b = &sbBlob{b: rcB, r: r, d: digest.Digest(r.Digest)} -// } else { -// rcB, err := s.rc.BlobGet(s.ctx, r, digest.Digest(r.Digest)) -// if err != nil { -// ls.RaiseError("Blob pull failed: %v", err) -// } -// b = &sbBlob{b: rcB, r: r, d: digest.Digest(r.Digest)} -// } -// case lua.LTUserData: -// ud := ls.CheckUserData(i) -// switch ud.Value.(type) { -// case *sbBlob: -// b = ud.Value.(*sbBlob) -// case *config: -// c := ud.Value.(*config) -// b = &sbBlob{b: c.conf, r: c.r, d: digest.Digest(c.r.Digest)} -// case *reference: -// r := ud.Value.(*reference).r -// if head { -// rcB, err := s.rc.BlobHead(s.ctx, r, digest.Digest(r.Digest)) -// if err != nil { -// ls.RaiseError("Failed retrieving \"%s\" blob: %v", r.CommonName(), err) -// } -// b = &sbBlob{b: rcB, r: r, d: digest.Digest(r.Digest)} -// } else { -// rcB, err := s.rc.BlobGet(s.ctx, r, digest.Digest(r.Digest)) -// if err != nil { -// ls.RaiseError("Blob pull failed: %v", err) -// } -// b = &sbBlob{b: rcB, r: r, d: digest.Digest(r.Digest)} -// } -// default: -// ls.ArgError(i, "blob expected") -// } -// default: -// ls.ArgError(i, "blob expected") -// } -// return b -// } - func (s *Sandbox) blobGet(ls *lua.LState) int { err := s.ctx.Err() if err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/cmd/regbot/sandbox/manifest.go new/regclient-0.11.5/cmd/regbot/sandbox/manifest.go --- old/regclient-0.11.4/cmd/regbot/sandbox/manifest.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/cmd/regbot/sandbox/manifest.go 2026-05-26 15:04:56.000000000 +0200 @@ -24,6 +24,7 @@ luaManifestName, map[string]lua.LGFunction{ "__tostring": s.manifestJSON, + "descriptor": s.manifestDescriptor, "get": s.manifestGet, "getList": s.manifestGetList, "head": s.manifestHead, @@ -33,6 +34,7 @@ "__index": { "config": s.configGet, "delete": s.manifestDelete, + "descriptor": s.manifestDescriptor, "export": s.manifestExport, "get": s.manifestGet, "head": s.manifestHead, @@ -48,12 +50,12 @@ var m *sbManifest switch ls.Get(i).Type() { case lua.LTString: - r, err := ref.New(ls.CheckString(1)) + r, err := ref.New(ls.CheckString(i)) if err != nil { ls.RaiseError("reference parsing failed: %v", err) } if head { - rcM, err := s.rc.ManifestHead(s.ctx, r) + rcM, err := s.rc.ManifestHead(s.ctx, r, regclient.WithManifestRequireDigest()) if err != nil { ls.RaiseError("Failed retrieving \"%s\" manifest: %v", r.CommonName(), err) } @@ -76,7 +78,7 @@ case *reference: r := ud.Value.(*reference) if head { - rcM, err := s.rc.ManifestHead(s.ctx, r.r) + rcM, err := s.rc.ManifestHead(s.ctx, r.r, regclient.WithManifestRequireDigest()) if err != nil { ls.RaiseError("Failed retrieving \"%s\" manifest: %v", r.r.CommonName(), err) } @@ -125,6 +127,17 @@ return 0 } +func (s *Sandbox) manifestDescriptor(ls *lua.LState) int { + m := s.checkManifest(ls, 1, true, true) + if m == nil || m.m == nil { + ls.RaiseError("Failed retrieving manifest") + } + desc := m.m.GetDescriptor() + ud := go2lua.Export(ls, desc) + ls.Push(ud) + return 1 +} + func (s *Sandbox) manifestExport(ls *lua.LState) int { var newM *sbManifest i := 1 @@ -153,7 +166,6 @@ } // save image to a new manifest rcM, err := manifest.New(manifest.WithOrig(reflect.ValueOf(newMMP).Elem().Interface())) // reflect is needed again to deref the pointer now - // rcM, err := manifest.FromOrig(newMM) if err != nil { ls.RaiseError("Failed exporting manifest (from orig): %v", err) } @@ -220,7 +232,7 @@ slog.String("script", s.name), slog.String("image", r.r.CommonName())) - m, err := s.rc.ManifestHead(s.ctx, r.r) + m, err := s.rc.ManifestHead(s.ctx, r.r, regclient.WithManifestRequireDigest()) if err != nil { ls.RaiseError("Failed retrieving \"%s\" manifest: %v", r.r.CommonName(), err) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/cmd/regbot/sandbox/reference.go new/regclient-0.11.5/cmd/regbot/sandbox/reference.go --- old/regclient-0.11.4/cmd/regbot/sandbox/reference.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/cmd/regbot/sandbox/reference.go 2026-05-26 15:04:56.000000000 +0200 @@ -85,17 +85,6 @@ return r } -// func isReference(ls *lua.LState, i int) bool { -// if ls.Get(i).Type() != lua.LTUserData { -// return false -// } -// ud := ls.CheckUserData(i) -// if _, ok := ud.Value.(*reference); ok { -// return true -// } -// return false -// } - // referenceString converts a reference back to a common name func (s *Sandbox) referenceString(ls *lua.LState) int { r := s.checkReference(ls, 1) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/cmd/regbot/sandbox/sandbox.go new/regclient-0.11.5/cmd/regbot/sandbox/sandbox.go --- old/regclient-0.11.4/cmd/regbot/sandbox/sandbox.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/cmd/regbot/sandbox/sandbox.go 2026-05-26 15:04:56.000000000 +0200 @@ -121,6 +121,7 @@ } } +// setupMod configures a namespace, attaches various functions, and attaches tables of metamethods func (s *Sandbox) setupMod(name string, funcs map[string]lua.LGFunction, tables map[string]map[string]lua.LGFunction) { mt := s.ls.NewTypeMetatable(name) s.ls.SetGlobal(name, mt) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/cmd/regbot/sandbox/sandbox_test.go new/regclient-0.11.5/cmd/regbot/sandbox/sandbox_test.go --- old/regclient-0.11.4/cmd/regbot/sandbox/sandbox_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/regclient-0.11.5/cmd/regbot/sandbox/sandbox_test.go 2026-05-26 15:04:56.000000000 +0200 @@ -0,0 +1,133 @@ +package sandbox + +import ( + "errors" + "net/http/httptest" + "net/url" + "testing" + + "github.com/olareg/olareg" + oConfig "github.com/olareg/olareg/config" + + "github.com/regclient/regclient" + rcConfig "github.com/regclient/regclient/config" +) + +func TestSandbox(t *testing.T) { + t.Parallel() + ctx := t.Context() + boolT := true + regHandler := olareg.New(oConfig.Config{ + Storage: oConfig.ConfigStorage{ + StoreType: oConfig.StoreMem, + RootDir: "../../../testdata", + }, + API: oConfig.ConfigAPI{ + DeleteEnabled: &boolT, + }, + }) + ts := httptest.NewServer(regHandler) + tsURL, _ := url.Parse(ts.URL) + tsHost := tsURL.Host + t.Cleanup(func() { + ts.Close() + _ = regHandler.Close() + }) + rcHosts := []rcConfig.Host{ + { + Name: tsHost, + Hostname: tsHost, + TLS: rcConfig.TLSDisabled, + }, + { + Name: "registry.example.org", + Hostname: tsHost, + TLS: rcConfig.TLSDisabled, + }, + } + // replace regclient with one configured for test hosts + rc := regclient.New( + regclient.WithConfigHost(rcHosts...), + ) + s := New("test", WithContext(ctx), WithRegClient(rc)) + + tt := []struct { + name string + script string + expectErr error + }{ + { + name: "Empty", + script: "", + }, + { + name: "List tags", + script: ` + tags = tag.ls("registry.example.org/testrepo") + for k, t in pairs(tags) do + if t == "v2" then + return + end + end + error("v2 tag was seen in the listing") + `, + }, + { + name: "Find duplicate digest to mirror tag", + script: ` + target = manifest.descriptor("registry.example.org/testrepo:mirror").Digest + tags = tag.ls("registry.example.org/testrepo") + for k, t in pairs(tags) do + if t ~= "mirror" and target == manifest.descriptor("registry.example.org/testrepo:" .. t).Digest then + log("found matching tag to mirror: " .. t) + return + end + end + error("did not find matching tag to mirror tag") + `, + }, + { + name: "Manifest descriptor", + script: ` + m = manifest.getList("registry.example.org/testrepo:v1") + if m:descriptor().MediaType ~= "application/vnd.oci.image.index.v1+json" then + error("v1 media type is " .. m:descriptor().MediaType) + end + if not string.match(m:descriptor().Digest, "^sha256:") then + error("v1 digest is " .. m:descriptor().Digest) + end + -- get the descriptor directly + desc = manifest.descriptor("registry.example.org/testrepo:v2") + if desc.MediaType ~= "application/vnd.oci.image.index.v1+json" then + error("v2 media type is " .. desc.MediaType) + end + `, + }, + { + name: "Get config", + script: ` + m = manifest.get("registry.example.org/testrepo:v1", "linux/amd64") + ic = image.config(m) + if ic.Config.Labels["version"] ~= "1" then + error("version label missing/invalid: " .. ic.Config.Labels["version"]) + end`, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + err := s.RunScript(tc.script) + if tc.expectErr != nil { + if err == nil { + t.Errorf("process did not fail") + } else if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() { + t.Errorf("unexpected error on process: %v, expected %v", err, tc.expectErr) + } + return + } + if err != nil { + t.Fatalf("unexpected error on process: %v", err) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/go.mod new/regclient-0.11.5/go.mod --- old/regclient-0.11.4/go.mod 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/go.mod 2026-05-26 15:04:56.000000000 +0200 @@ -13,7 +13,7 @@ github.com/spf13/cobra v1.10.2 github.com/ulikunitz/xz v0.5.15 github.com/yuin/gopher-lua v1.1.2 - golang.org/x/sys v0.44.0 + golang.org/x/sys v0.45.0 golang.org/x/term v0.43.0 ) @@ -21,5 +21,5 @@ github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/sudo-bmitch/oci-digest v0.1.2 // indirect - golang.org/x/crypto v0.51.0 // indirect + golang.org/x/crypto v0.52.0 // indirect ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/go.sum new/regclient-0.11.5/go.sum --- old/regclient-0.11.4/go.sum 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/go.sum 2026-05-26 15:04:56.000000000 +0200 @@ -34,10 +34,10 @@ github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/internal/auth/auth.go new/regclient-0.11.5/internal/auth/auth.go --- old/regclient-0.11.4/internal/auth/auth.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/internal/auth/auth.go 2026-05-26 15:04:56.000000000 +0200 @@ -8,7 +8,6 @@ "fmt" "io" "log/slog" - "net" "net/http" "net/url" "slices" @@ -16,6 +15,7 @@ "sync" "time" + "github.com/regclient/regclient/internal/regnet" "github.com/regclient/regclient/types/errs" ) @@ -641,16 +641,8 @@ b.tokenURL = u } // verify tokenURL is allowed for request URL - if req.URL.Scheme == "https" && b.tokenURL.Scheme != "https" { - return fmt.Errorf("downgrading to an http token server from an https registry is not allowed%.0w", errs.ErrHTTPUnauthorized) - } - hostToken := b.tokenURL.Hostname() - hostReq := req.URL.Hostname() - ipToken := net.ParseIP(hostToken) - ipReq := net.ParseIP(hostReq) - if ipToken != nil && (ipToken.IsLoopback() || ipToken.IsLinkLocalUnicast() || ipToken.IsLinkLocalMulticast()) && - (ipReq == nil || !(ipReq.IsLoopback() || ipReq.IsLinkLocalUnicast() || ipReq.IsLinkLocalMulticast())) { - return fmt.Errorf("requesting a local token server from a non-local registry is not allowed%.0w", errs.ErrHTTPUnauthorized) + if err := regnet.AllowRedirect(*req.URL, *b.tokenURL); err != nil { + return err } // if unexpired token already exists, return it if b.token.Token != "" && !b.isExpired() { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/internal/auth/auth_test.go new/regclient-0.11.5/internal/auth/auth_test.go --- old/regclient-0.11.4/internal/auth/auth_test.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/internal/auth/auth_test.go 2026-05-26 15:04:56.000000000 +0200 @@ -334,7 +334,7 @@ handleRequest: &http.Request{ URL: externURL, }, - wantErrReq: errs.ErrHTTPUnauthorized, + wantErrReq: errs.ErrHTTPRedirectRefused, }, { name: "reject http downgrade", @@ -356,7 +356,7 @@ handleRequest: &http.Request{ URL: &tsHTTPS, }, - wantErrReq: errs.ErrHTTPUnauthorized, + wantErrReq: errs.ErrHTTPRedirectRefused, }, } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/internal/reghttp/http.go new/regclient-0.11.5/internal/reghttp/http.go --- old/regclient-0.11.4/internal/reghttp/http.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/internal/reghttp/http.go 2026-05-26 15:04:56.000000000 +0200 @@ -30,6 +30,7 @@ "github.com/regclient/regclient/config" "github.com/regclient/regclient/internal/auth" "github.com/regclient/regclient/internal/pqueue" + "github.com/regclient/regclient/internal/regnet" "github.com/regclient/regclient/internal/reqmeta" "github.com/regclient/regclient/types" "github.com/regclient/regclient/types/errs" @@ -401,7 +402,8 @@ } // add auth headers err = hAuth.UpdateRequest(httpReq) - if err != nil { + // abort on auth errors, but only after the first request has been attempted + if err != nil && resp.resp != nil { if errors.Is(err, errs.ErrHTTPUnauthorized) { dropHost = true } else { @@ -557,7 +559,7 @@ // Read provides a retryable read from the body of the response. func (resp *Resp) Read(b []byte) (int, error) { - if resp.done { + if resp.done || resp.reader == nil { return 0, io.EOF } if resp.resp == nil { @@ -814,6 +816,11 @@ if len(via) >= 10 { return errors.New("stopped after 10 redirects") } + // verify redirect is allowed + last := via[len(via)-1] + if err := regnet.AllowRedirect(*last.URL, *req.URL); err != nil { + return err + } // add auth headers if appropriate for the target host hAuth := ch.getAuth(repo) err := hAuth.UpdateRequest(req) @@ -851,8 +858,13 @@ return auth.DefaultCredsFn } return func(h string) auth.Cred { - hCred := ch.config.GetCred() - return auth.Cred{User: hCred.User, Password: hCred.Password, Token: hCred.Token} + // only return credentials to challenges from the registry server, not to any redirects + if h == ch.config.Hostname { + hCred := ch.config.GetCred() + return auth.Cred{User: hCred.User, Password: hCred.Password, Token: hCred.Token} + } else { + return auth.Cred{} + } } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/internal/reghttp/http_test.go new/regclient-0.11.5/internal/reghttp/http_test.go --- old/regclient-0.11.4/internal/reghttp/http_test.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/internal/reghttp/http_test.go 2026-05-26 15:04:56.000000000 +0200 @@ -929,6 +929,11 @@ "expectns." + ts2Host, }, }, + "external-host.example.org": { + Name: "external-host.example.org", + Hostname: "external-host.example.org", + TLS: config.TLSDisabled, + }, } // create APIs for requests to run @@ -1217,6 +1222,42 @@ t.Errorf("error closing request: %v", err) } }) + // test redirect where auth should not be sent + t.Run("redirect-without-auth", func(t *testing.T) { + u, err := tsURL.Parse("/v2/project/manifests/tag-auth") + if err != nil { + t.Fatalf("failed to parse url: %v", err) + } + authReq := &Req{ + Host: "external-host.example.org", + Method: "GET", + Repository: "project-redirect", + Path: "manifests/tag-auth", + DirectURL: u, + Headers: headers, + } + resp, err := hc.Do(ctx, authReq) + if !errors.Is(err, errs.ErrHTTPUnauthorized) { + t.Errorf("expected Unauthorized, received %v", err) + } + if resp == nil { + t.Errorf("response missing") + } else { + if resp.HTTPResponse().StatusCode != http.StatusUnauthorized { + t.Errorf("invalid status code, expected %d, received %d", http.StatusUnauthorized, resp.HTTPResponse().StatusCode) + } + body, err := io.ReadAll(resp) + if err != nil { + t.Fatalf("body read failure: %v", err) + } else if bytes.Equal(body, getBody) { + t.Errorf("body received without auth") + } + err = resp.Close() + if err != nil { + t.Errorf("error closing request: %v", err) + } + } + }) // test repoauth t.Run("RepoAuth", func(t *testing.T) { authReq1G := &Req{ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/internal/regnet/regnet.go new/regclient-0.11.5/internal/regnet/regnet.go --- old/regclient-0.11.4/internal/regnet/regnet.go 1970-01-01 01:00:00.000000000 +0100 +++ new/regclient-0.11.5/internal/regnet/regnet.go 2026-05-26 15:04:56.000000000 +0200 @@ -0,0 +1,48 @@ +// Package regnet contains networking helper functions for interacting with registries. +package regnet + +import ( + "fmt" + "net" + "net/url" + + "github.com/regclient/regclient/types/errs" +) + +func AllowRedirect(src, dest url.URL) error { + if src.Scheme == "https" && dest.Scheme != "https" { + return fmt.Errorf("redirect from an https to non-https server is not allowed (%s)%.0w", dest.String(), errs.ErrHTTPRedirectRefused) + } + if !IsLocal(src.Host) && IsLocal(dest.Host) { + return fmt.Errorf("redirect to a local domain is not allowed (%s)%.0w", dest.String(), errs.ErrHTTPRedirectRefused) + } + return nil +} + +func IsLocal(hostPort string) bool { + // strip trailing port + host, _, err := net.SplitHostPort(hostPort) + if err != nil { + host = hostPort + } + // parse IP + ip := net.ParseIP(host) + if ip != nil { + return isIPLocal(ip) + } + // else resolve the hostname and then check each IP + ips, err := net.LookupIP(host) + if err != nil { + return false + } + for _, ip := range ips { + if ip != nil && isIPLocal(ip) { + return true + } + } + return false +} + +func isIPLocal(ip net.IP) bool { + return ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsUnspecified() +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/internal/regnet/regnet_test.go new/regclient-0.11.5/internal/regnet/regnet_test.go --- old/regclient-0.11.4/internal/regnet/regnet_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/regclient-0.11.5/internal/regnet/regnet_test.go 2026-05-26 15:04:56.000000000 +0200 @@ -0,0 +1,101 @@ +package regnet + +import ( + "net/url" + "testing" + + "github.com/regclient/regclient/types/errs" +) + +func TestAllowRedirect(t *testing.T) { + tt := []struct { + name string + src, dest url.URL + expect error + }{ + { + name: "http to https", + src: urlMustParse(t, "http://registry.example.org"), + dest: urlMustParse(t, "https://token.example.org"), + expect: nil, + }, + { + name: "https to http", + src: urlMustParse(t, "https://registry.example.org"), + dest: urlMustParse(t, "http://token.example.org"), + expect: errs.ErrHTTPRedirectRefused, + }, + { + name: "external to local", + src: urlMustParse(t, "http://10.0.0.1"), + dest: urlMustParse(t, "http://127.0.0.5"), + expect: errs.ErrHTTPRedirectRefused, + }, + { + name: "local to external", + src: urlMustParse(t, "http://127.0.0.5"), + dest: urlMustParse(t, "http://10.0.0.1"), + expect: nil, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + result := AllowRedirect(tc.src, tc.dest) + if (result == nil && tc.expect != nil) || (result != nil && tc.expect == nil) { + t.Errorf("expected %v, received %v", tc.expect, result) + } + }) + } +} + +func urlMustParse(t *testing.T, s string) url.URL { + u, err := url.Parse(s) + if err != nil { + t.Fatalf("failed to parse url %s: %v", s, err) + } + return *u +} + +func TestIsLocal(t *testing.T) { + tt := []struct { + host string + expect bool + }{ + { + host: "127.0.0.2", + expect: true, + }, + { + host: "::1", + expect: true, + }, + { + host: "[::1]:8080", + expect: true, + }, + { + host: "localhost.", + expect: true, + }, + { + host: "0.0.0.0:8080", + expect: true, + }, + { + host: "10.0.0.1", + expect: false, + }, + { + host: "regclient.org", + expect: false, + }, + } + for _, tc := range tt { + t.Run(tc.host, func(t *testing.T) { + result := IsLocal(tc.host) + if result != tc.expect { + t.Errorf("expected %t, received %t", tc.expect, result) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/release.md new/regclient-0.11.5/release.md --- old/regclient-0.11.4/release.md 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/release.md 2026-05-26 15:04:56.000000000 +0200 @@ -1,28 +1,19 @@ -# Release v0.11.4 +# Release v0.11.5 Security: -- Validate server URL in token auth. ([PR 1075][pr-1075]) -- Upgrading Go fixes CVE-2026-33814 and CVE-2026-39836, other vulnerabilities fixed in 1.26.3 were not called by this project. ([PR 1084][pr-1084]) +- Prevent https to non-https downgrades and localhost redirects. ([PR 1093][pr-1093]) +- Forbid sending auth on redirects. ([PR 1095][pr-1095]) Features: -- Support scanning OCI Layout for referrers. ([PR 1074][pr-1074]) -- Add created timestamp in OCI Layout entries. ([PR 1081][pr-1081]) -- `tag.ls` now accepts the same pagination parameters as `repo.ls`. ([PR 1086][pr-1086]) - -Fixes: - -- Push tags for minor and major releases on Docker Hub. ([PR 1087][pr-1087]) +- Add regbot `manifest.descriptor` to the sandbox. ([PR 1091][pr-1091]) Contributors: -- @ffried +- @GimmyDatBeeR - @sudo-bmitch -[pr-1074]: https://github.com/regclient/regclient/pull/1074 -[pr-1075]: https://github.com/regclient/regclient/pull/1075 -[pr-1081]: https://github.com/regclient/regclient/pull/1081 -[pr-1084]: https://github.com/regclient/regclient/pull/1084 -[pr-1086]: https://github.com/regclient/regclient/pull/1086 -[pr-1087]: https://github.com/regclient/regclient/pull/1087 +[pr-1091]: https://github.com/regclient/regclient/pull/1091 +[pr-1093]: https://github.com/regclient/regclient/pull/1093 +[pr-1095]: https://github.com/regclient/regclient/pull/1095 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/scheme/reg/blob.go new/regclient-0.11.5/scheme/reg/blob.go --- old/regclient-0.11.4/scheme/reg/blob.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/scheme/reg/blob.go 2026-05-26 15:04:56.000000000 +0200 @@ -19,6 +19,7 @@ "github.com/opencontainers/go-digest" "github.com/regclient/regclient/internal/reghttp" + "github.com/regclient/regclient/internal/regnet" "github.com/regclient/regclient/internal/reqmeta" "github.com/regclient/regclient/types/blob" "github.com/regclient/regclient/types/descriptor" @@ -68,6 +69,11 @@ if err != nil { return nil, fmt.Errorf("failed to parse external url \"%s\": %w", curURL, err) } + // refuse requests to local URLs unless registry is also local + // Note: the AllowRedirect check is not used because blobs could be hosted on an http CDN and data is content addressable + if regnet.IsLocal(u.Host) && !regnet.IsLocal(req.Host) { + return nil, fmt.Errorf("refusing to redirect blob request to a local URL: %s%.0w", curURL, errs.ErrHTTPRedirectRefused) + } req = ®http.Req{ MetaKind: reqmeta.Blob, Host: r.Registry, @@ -527,6 +533,9 @@ resp.HTTPResponse().Header.Get("Location") != "" && resp.HTTPResponse().Header.Get("Range") != "" { retryCur++ + if retryCur > retryLimit { + return d, fmt.Errorf("failed to send blob (chunk), ref %s: http status: %w", r.CommonName(), reghttp.HTTPError(resp.HTTPResponse().StatusCode)) + } reg.slog.Debug("Recoverable chunk upload error", slog.String("ref", r.CommonName()), slog.Int64("chunkStart", chunkStart), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/scheme/reg/blob_test.go new/regclient-0.11.5/scheme/reg/blob_test.go --- old/regclient-0.11.4/scheme/reg/blob_test.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/scheme/reg/blob_test.go 2026-05-26 15:04:56.000000000 +0200 @@ -222,6 +222,8 @@ Name: tsHost, Hostname: tsHost, TLS: config.TLSDisabled, + User: "username", + Pass: "secret", }, } log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelWarn})) @@ -347,6 +349,46 @@ } }) + t.Run("External Reject redirect", func(t *testing.T) { + r, err := ref.New("registry.example.org/proj/external") + if err != nil { + t.Fatalf("Failed creating ref: %v", err) + } + br, err := reg.BlobGet(ctx, r, descriptor.Descriptor{Digest: d1, URLs: []string{tsURL.Scheme + "://" + tsURL.Host + "/external/" + d1.String()}}) + if err == nil { + br.Close() + } + if !errors.Is(err, errs.ErrHTTPRedirectRefused) { + t.Fatalf("Redirect to a local URL was not rejected as expected: %v", err) + } + }) + + t.Run("Detect external credential leak", func(t *testing.T) { + extServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "" { + t.Errorf("credential leak detected: %s", auth) + w.WriteHeader(http.StatusOK) + w.Write(blob1) + } else { + w.Header().Add("WWW-Authenticate", "Basic realm=\"External Blob Host\"") + w.WriteHeader(http.StatusUnauthorized) + } + })) + defer extServer.Close() + r, err := ref.New(tsURL.Host + externalRepo) + if err != nil { + t.Fatalf("Failed creating ref: %v", err) + } + br, err := reg.BlobGet(ctx, r, descriptor.Descriptor{Digest: d1, URLs: []string{extServer.URL + "/external/" + d1.String()}}) + if err == nil { + br.Close() + } + if !errors.Is(err, errs.ErrHTTPUnauthorized) { + t.Fatalf("Unauthorized status not received, err received: %v", err) + } + }) + t.Run("Missing", func(t *testing.T) { r, err := ref.New(tsURL.Host + blobRepo) if err != nil { @@ -414,6 +456,7 @@ blobLen3 := 1000 // blob without a full final chunk blobLen4 := 2048 // must be blobChunk < blobLen <= blobChunk * 2 blobLen5 := 500 // single chunk + blobLen7 := 1024 d1, blob1 := reqresp.NewRandomBlob(blobLen, seed) d2, blob2 := reqresp.NewRandomBlob(blobLen, seed+1) d2Bad := digest.SHA256.FromString("digest 2 bad") @@ -422,6 +465,7 @@ d5, blob5 := reqresp.NewRandomBlob(blobLen5, seed+4) blob6 := []byte{} d6 := digest.SHA256.FromBytes(blob6) + d7, blob7 := reqresp.NewRandomBlob(blobLen7, seed+5) // external auth refused blob d1sha512 := digest.SHA512.FromBytes(blob1) d5sha512 := digest.SHA512.FromBytes(blob5) uuid1 := reqresp.NewRandomID(seed + 10) @@ -431,6 +475,7 @@ uuid4 := reqresp.NewRandomID(seed + 14) uuid5 := reqresp.NewRandomID(seed + 15) uuid6 := reqresp.NewRandomID(seed + 16) + uuid7 := reqresp.NewRandomID(seed + 17) // dMissing := digest.FromBytes([]byte("missing")) user := "testing" pass := "password" @@ -448,7 +493,6 @@ Headers: http.Header{ "Content-Length": {fmt.Sprintf("%d", len(blob1))}, "Content-Type": {"application/octet-stream"}, - "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user+":"+pass)))}, }, Body: blob1, }, @@ -463,11 +507,11 @@ }, { ReqEntry: reqresp.ReqEntry{ - Name: "PUT for d1 unauth", + Name: "PUT for d1 sha512", Method: "PUT", - Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid1, + Path: "/v2" + blobRepo1sha512 + "/blobs/uploads/" + uuid1, Query: map[string][]string{ - "digest": {d1.String()}, + "digest": {d1sha512.String()}, }, Headers: http.Header{ "Content-Length": {fmt.Sprintf("%d", len(blob1))}, @@ -476,6 +520,24 @@ Body: blob1, }, RespEntry: reqresp.RespEntry{ + Status: http.StatusCreated, + Headers: http.Header{ + "Content-Length": {"0"}, + "Location": {"/v2" + blobRepo + "/blobs/" + d1.String()}, + "Docker-Content-Digest": {d1sha512.String()}, + }, + }, + }, + { + ReqEntry: reqresp.ReqEntry{ + Name: "GET for d7 unauth", + Method: "GET", + Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid7, + Query: map[string][]string{ + "digest": {d7.String()}, + }, + }, + RespEntry: reqresp.RespEntry{ Status: http.StatusUnauthorized, Headers: http.Header{ "WWW-Authenticate": {"Basic realm=\"testing\""}, @@ -484,41 +546,55 @@ }, { ReqEntry: reqresp.ReqEntry{ - Name: "PUT for d1 sha512", + Name: "DELETE for d7 unauth", + Method: "DELETE", + Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid7, + Query: map[string][]string{ + "digest": {d7.String()}, + }, + }, + RespEntry: reqresp.RespEntry{ + Status: http.StatusUnauthorized, + Headers: http.Header{ + "WWW-Authenticate": {"Basic realm=\"testing\""}, + }, + }, + }, + { + ReqEntry: reqresp.ReqEntry{ + Name: "PUT for d7 unauth", Method: "PUT", - Path: "/v2" + blobRepo1sha512 + "/blobs/uploads/" + uuid1, + Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid7, Query: map[string][]string{ - "digest": {d1sha512.String()}, + "digest": {d7.String()}, }, Headers: http.Header{ - "Content-Length": {fmt.Sprintf("%d", len(blob1))}, + "Content-Length": {fmt.Sprintf("%d", len(blob7))}, "Content-Type": {"application/octet-stream"}, - "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user+":"+pass)))}, }, - Body: blob1, + Body: blob7, }, RespEntry: reqresp.RespEntry{ - Status: http.StatusCreated, + Status: http.StatusUnauthorized, Headers: http.Header{ - "Content-Length": {"0"}, - "Location": {"/v2" + blobRepo + "/blobs/" + d1.String()}, - "Docker-Content-Digest": {d1sha512.String()}, + "WWW-Authenticate": {"Basic realm=\"testing\""}, }, }, }, { ReqEntry: reqresp.ReqEntry{ - Name: "PUT for d1 sha512 unauth", - Method: "PUT", - Path: "/v2" + blobRepo1sha512 + "/blobs/uploads/" + uuid1, + Name: "PATCH for d7 unauth", + Method: "PATCH", + Path: "/v2" + blobRepo + "/blobs/uploads/" + uuid7, Query: map[string][]string{ - "digest": {d1sha512.String()}, + "digest": {d7.String()}, }, Headers: http.Header{ - "Content-Length": {fmt.Sprintf("%d", len(blob1))}, + "Content-Length": {fmt.Sprintf("%d", blobChunk)}, + "Content-Range": {fmt.Sprintf("0-%d", blobChunk-1)}, "Content-Type": {"application/octet-stream"}, }, - Body: blob1, + Body: blob7[0:blobChunk], }, RespEntry: reqresp.RespEntry{ Status: http.StatusUnauthorized, @@ -1337,6 +1413,44 @@ }, }, }, + // upload put for d7 + { + ReqEntry: reqresp.ReqEntry{ + Name: "POST for d7", + Method: "POST", + Path: "/v2" + blobRepo + "/blobs/uploads/", + Query: map[string][]string{ + "mount": {d7.String()}, + }, + Headers: http.Header{ + "Authorization": {fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(user+":"+pass)))}, + }, + }, + RespEntry: reqresp.RespEntry{ + Status: http.StatusAccepted, + Headers: http.Header{ + "Content-Length": {"0"}, + "Location": {fmt.Sprintf("http://%s/v2%s/blobs/uploads/%s", blobHost, blobRepo, uuid7)}, + }, + }, + }, + { + ReqEntry: reqresp.ReqEntry{ + Name: "POST for d7 unauth", + Method: "POST", + Path: "/v2" + blobRepo + "/blobs/uploads/", + Query: map[string][]string{ + "mount": {d7.String()}, + }, + }, + RespEntry: reqresp.RespEntry{ + Status: http.StatusUnauthorized, + Headers: http.Header{ + "Content-Length": {"0"}, + "WWW-Authenticate": {"Basic realm=\"testing\""}, + }, + }, + }, } rrs = append(rrs, reqresp.BaseEntries...) // create a server @@ -1560,5 +1674,18 @@ } }) + // external blob server should not request auth creds + t.Run("External request for auth", func(t *testing.T) { + r, err := ref.New(tsURL.Host + blobRepo) + if err != nil { + t.Fatalf("Failed creating ref: %v", err) + } + br := bytes.NewReader(blob7) + _, err = reg.BlobPut(ctx, r, descriptor.Descriptor{Digest: d7, Size: int64(len(blob7))}, br) + if !errors.Is(err, errs.ErrHTTPUnauthorized) { + t.Fatalf("BlobPut to external server requesting auth, expected: %v, received: %v", errs.ErrHTTPUnauthorized, err) + } + }) + // TODO: test failed mount (blobGetUploadURL) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/regclient-0.11.4/types/errs/error.go new/regclient-0.11.5/types/errs/error.go --- old/regclient-0.11.4/types/errs/error.go 2026-05-11 20:52:04.000000000 +0200 +++ new/regclient-0.11.5/types/errs/error.go 2026-05-26 15:04:56.000000000 +0200 @@ -24,6 +24,8 @@ ErrFileDeleted = errors.New("file deleted") // ErrFileNotFound indicates a requested file is not found ErrFileNotFound = fmt.Errorf("file not found%.0w", fs.ErrNotExist) + // ErrHTTPRedirectRefused indicates a request to redirect to a local host or from https to non-https + ErrHTTPRedirectRefused = fmt.Errorf("refusing to redirect to a local or non-https url") // ErrHTTPStatus if the http status code was unexpected ErrHTTPStatus = errors.New("unexpected http status code") // ErrInvalidChallenge indicates an issue with the received challenge in the WWW-Authenticate header ++++++ regclient.obsinfo ++++++ --- /var/tmp/diff_new_pack.A4VFbs/_old 2026-05-29 18:10:43.015404608 +0200 +++ /var/tmp/diff_new_pack.A4VFbs/_new 2026-05-29 18:10:43.023404952 +0200 @@ -1,5 +1,5 @@ name: regclient -version: 0.11.4 -mtime: 1778525524 -commit: 8b080b448b5f7dac833fd14e5e9979d96e164164 +version: 0.11.5 +mtime: 1779800696 +commit: 2bc542b4a19d6e4fd939be150f1443da2395690c ++++++ vendor.tar.gz ++++++ ++++ 4081 lines of diff (skipped)
