# @sigstore/oci sends the registry Authorization header to a cross-host blob-upload Location

@sigstore/oci attaches the registry Authorization header via client defaults, then PUTs blobs to an absolute cross-host upload Location from the registry response, leaking credentials to a hostile registry.

- Payout: Research
- Severity: high
- Program: sigstore-js
- Found by: arpitjain099
- Disclosed: 2026-10-30
- CVE: CVE-2026-118763
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-j63v-mwhm-56jx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-j63v-mwhm-56jx
- Tags: area:web3 domain:oss domain:web3 impact:data-leak kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:misconfiguration vuln:secrets-leak

**Target:** npm @sigstore/oci RegistryClient blob upload (registry.ts)
**Root cause:** signIn() installs Authorization (Basic/Bearer/creds.headers) as a client-wide default via .defaults(), and uploadBlob() PUTs to an absolute Location parsed from the registry response without checking host or scheme equality against the registry.
**Steps:**
1. Trace attachArtifactToImage() -> OCIImage.addArtifact() -> client.signIn(credentials), which sets `this.#fetch = this.#fetch.defaults({ headers: { [HEADER_AUTHORIZATION]: 'Basic ' + basicAuth } })` (or Bearer).
2. POST to `.../blobs/uploads/`; a hostile registry returns an absolute `Location` header pointing to an attacker host.
3. uploadBlob() builds `uploadLocation = new URL(location.startsWith('/') ? this.#baseURL + location : location)` — absolute Location used verbatim.
4. `await this.#fetch(uploadLocation.href, { method: 'PUT', body: blob, headers: { [HEADER_CONTENT_TYPE]: ... } })` sends the default Authorization header to the attacker host.
5. Capture the Basic username:password or Bearer token on the attacker host.
**How found:** Enumerated every place @sigstore/oci attaches a credential to an outbound request and checked whether the request host can be steered by a server response; noticed the blob-upload Location path attaches the client-default Authorization to a PUT whose host comes from the registry response, and that the prior GHSA-pf56 fix touched only credential selection (credentials.ts), not the send path (registry.ts).
**Exploitation:** A malicious or impersonated OCI registry (or one legitimately redirecting blob storage to third-party hosts) returns an absolute cross-host Location on blob-upload POST; the library PUTs the blob there with the registry credential attached, handing the attacker the raw Basic credentials or bearer token.
**Insight:** When a library sets auth via client-wide .defaults(), every later request — including ones to URLs parsed from response headers like Location — inherits it; fetch-layer redirect stripping does not cover fresh explicit calls to response-supplied URLs. Audit prior fixes for scope: a credential-selection patch leaves the credential-send path untouched.

## Proof of concept (copied exactly from the original report)

**uploadBlob PUT to response-supplied Location**

```
const uploadLocation = new URL(
  location.startsWith('/') ? `${this.#baseURL}${location}` : location
);
...
await this.#fetch(uploadLocation.href, { method: 'PUT', ... });
```

**signIn installs Authorization as client default**

```
126-127: this.#fetch = this.#fetch.defaults({ headers: { [HEADER_AUTHORIZATION]: `Basic ${basicAuth}` } })
145-146: this.#fetch = this.#fetch.defaults({ headers: { [HEADER_AUTHORIZATION]: `Bearer ${token}` } })
104:     this.#fetch = this.#fetch.defaults({ headers: creds.headers })
```

