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 = &reghttp.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)

Reply via email to