high · 7.4 Patched

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

CVE-2026-118763 · GHSA-j63v-mwhm-56jx · published 30 Oct 2026

How serious is it

  • Severity: high, CVSS score 7.4 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-118763?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • @sigstore/oci (npm) <= 0.7.1

How to check

  1. Run npm ls @sigstore/oci in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade @sigstore/oci to 0.7.2 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

### Summary @sigstore/oci sends the registry Authorization header to a cross-host blob-upload Location ## Affected versions and vulnerable location - Confirmed on sigstore-js at `3a83271` (current HEAD). - File: `packages/oci/src/registry.ts`. - `signIn()` installs the credential as a DEFAULT header on the shared client: `this.#fetch = this.#fetch.defaults({ headers: { [HEADER_AUTHORIZATION]: 'Basic ' + basicAuth } })` (`:126-128`) or `... 'Bearer ' + token` (`:145-147`), plus `creds.headers` from the Docker config (`:104`). - `uploadBlob()` reads the `Location` response header (`:184`), builds `uploadLocation = new URL(location.startsWith('/') ? this.#baseURL + location : location)` (`:190-192`), and PUTs the blob to it through the same client: `await this.#fetch(uploadLocation.href, { method: 'PUT', body: blob, headers: { [HEADER_CONTENT_TYPE]: ... } })` (`:198`). - There is no check that `uploadLocation`'s host (or scheme) equals the registry host before the PUT is sent. ## Root cause Because `signIn()` sets `Authorization` via `.defaults()`, it is attached to every request the client issues, including requests to a host the client read out of a server response. When a POST to `.../blobs/uploads/` returns an absolute `Location` pointing to a different host, `uploadBlob()` PUTs the blob there with the registry `Authorization` header still attached. If the challenge scheme was `basic`, that header carries the raw registry `username:password`; if bearer, it carries the access token; `creds.headers` custom auth headers ride along too. The library's HTTP layer (`make-fetch-happen` / `minipass-fetch`) strips `Authorization` on cross-host redirects it follows internally, which protects the `HEAD ... { redirect: 'follow' }` at `:167`. It does not help here, because the PUT to the upload `Location` is a fresh, explicit `this.#fetch()` call, not a followed redirect, so the default header is applied unconditionally. This is a residual of GHSA-pf56-329r-95rw / CVE-2026-59891 (credential confusion in `@sigstore/oci`). The fix for that advisory (commit `85c5838`) changed only `credentials.ts`, replacing a `key.includes(registry)` substring match with an exact host match when SELECTING which Docker credential to load. It never touched `registry.ts`, which controls WHERE the loaded credential is sent. The cross-host send remains. ## Attacker model A user pushing a signed artifact/attestation to an OCI registry via `attachArtifactToImage()`. The credential leaks to any host the registry names in the blob-upload `Location`. This is reachable when the registry is malicious or compromised, or when a registry that legitimately redirects blob uploads to third-party storage is impersonated. The OCI distribution spec permits absolute upload `Location` values (registries commonly redirect blob storage to a separate host), so a compliant-but-hostile registry can steer the PUT to an attacker host and capture the credential. This is the same "attacker-controlled registry" threat model accepted for GHSA-pf56. ## Reachability trace `index.ts:43 attachArtifactToImage()` -> `new OCIImage(...).addArtifact()` -> `image.ts:67 this.#client.signIn(credentials)` (installs the default Authorization header) -> `image.ts:76/79 this.#client.uploadBlob(...)` -> `registry.ts:198` PUT to the response-supplied `Location` with the default Authorization header attached. Same `RegistryClient` instance throughout, so the default persists. ## Reproduction Source-verified. No live push against a hostile registry was executed in this pass. Confirmed: ```bash rg -n "defaults\(|HEADER_AUTHORIZATION|Location|new URL\(|this.#fetch\(uploadLocation" packages/oci/src/registry.ts git log --oneline -3 -- packages/oci/src/credentials.ts packages/oci/src/registry.ts ``` `signIn()` sets `Authorization` via `.defaults()`; `uploadBlob()` PUTs to `uploadLocation.href` (which may be an absolute cross-host URL) with no host check; the GHSA-pf56 fix commit is the latest change to `credentials.ts`, while `registry.ts` has only dependency bumps since. ## Suggested fix Do not send the registry `Authorization` (or `creds.headers`) to an upload `Location` on a different host. Compare `uploadLocation.host` (and require `https`) against the registry host before the PUT; if it differs, issue the PUT without the credential (matching how registries that redirect to pre-signed storage URLs expect the upload to be unauthenticated), or explicitly re-authenticate for that host. More generally, avoid attaching the credential via a client-wide `.defaults()` and instead attach it per-request only to the registry host. ## How it was found and a note on tooling Every place `@sigstore/oci` was traced to attach a credential to an outbound request and checked whether the request host can be steered by a server response. The blob-upload `Location` path attaches the client-default `Authorization` to a PUT whose host comes from the registry response, with no host-equality check, and the GHSA-pf56 fix touched only the credential-selection code, not this send path. I used AI assistance for the trace and drafting and manually verified the cited lines and the fix scope at HEAD `3a83271`. Verification is source-only; I did not execute a push against a hostile registry. ## Independent re-verification at HEAD (2026-07-31) Re-read at HEAD `3a83271`. Unchanged, and the two halves sit in the same file: ``` 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 }) ``` and in `uploadBlob()`: ```ts const uploadLocation = new URL( location.startsWith('/') ? `${this.#baseURL}${location}` : location ); ... await this.#fetch(uploadLocation.href, { method: 'PUT', ... }); ``` The ternary is the whole issue. A relative `Location` is anchored to `#…

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database, OSV. Last updated 31 Oct 2026. All vulnerabilities · RSS · JSON