Repository navigation
# Security Report: Sensitive Configuration Values Leaked via PrometheusPropertiesException to HTTP Clients #2286
Description
Security Report: Sensitive Configuration Values Leaked via PrometheusPropertiesException to HTTP Clients
1. Vulnerability Summary
| Field | Value |
|---|---|
| Product | prometheus/client_java |
| Affected Version | 1.8.0 (main branch); all prior 1.x releases using prometheus-metrics-config are likely affected |
| Component | prometheus-metrics-config (root cause), prometheus-metrics-exporter-httpserver (disclosure vector) |
| Vulnerable File | prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java |
| Vulnerable Lines | 26-27, 90-91, 132-133, 148-149, 164-165, 196-197 |
| CWE | CWE-209: Generation of Error Message Containing Sensitive Information |
| CVSS 3.1 Score | 5.3 (Medium) |
| CVSS 3.1 Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
| Discoverer | Security Research |
2. Vulnerability Description
The Util class in the prometheus-metrics-config module embeds raw, unredacted configuration property values directly into PrometheusPropertiesException error messages. When a property value cannot be parsed as the expected type (integer, double, long, boolean, or double list), the exception message includes the full property key and its verbatim value using the pattern fullKey + "=" + property + ": Expecting <type> value".
PrometheusPropertiesException extends RuntimeException (unchecked), which means it propagates freely through the call stack without requiring explicit handling. When this exception reaches the HttpExchangeAdapter in the prometheus-metrics-exporter-httpserver module, the sendErrorResponseWithStackTrace() method serializes the complete exception -- including the exception message containing the raw property value and the full Java stack trace -- into an HTTP 500 response body and sends it directly to the requesting client.
This creates an information disclosure vulnerability: if a configuration property inadvertently contains sensitive data (API keys, database passwords, tokens) and that value fails type parsing, the sensitive value is leaked verbatim to any unauthenticated HTTP client that triggers the error path. The /metrics endpoint served by the built-in HTTPServer is accessible without authentication by default.
3. Affected Component Architecture
The vulnerability involves a two-stage pipeline: (1) raw values are embedded in exception messages at the configuration layer, and (2) those messages are forwarded unredacted to HTTP clients at the exporter layer.
Environment Variable / Properties File
|
| raw property value read
v
Util.loadInteger() / loadDouble() / loadLong() / loadBoolean() / loadDoubleList()
|
| parse fails -> PrometheusPropertiesException("key=RAW_VALUE: Expecting ...")
v
PrometheusPropertiesException (RuntimeException, unchecked)
|
| propagates unchecked through call stack
v
PrometheusScrapeHandler.handleRequest()
|
| catch (RuntimeException e) -> exchange.handleException(e)
v
HttpExchangeAdapter.sendErrorResponseWithStackTrace()
|
| exception.printStackTrace() -> HTTP 500 response body
v
HTTP Client receives full exception message + stack trace
(including the raw property value)
Key Design Flaws
- Root cause (Util.java): Raw property values are concatenated into exception messages instead of being redacted or truncated.
- Propagation enabler (PrometheusPropertiesException): Being an unchecked
RuntimeException, the exception bypasses all intermediate error handling. - Disclosure vector (HttpExchangeAdapter): The full stack trace, including the exception message with raw property values, is written to the HTTP response body.
4. Vulnerable Code
4.1 Root Cause: Raw Property Values in Exception Messages
File: prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/Util.java
All type-parsing methods in Util.java include the verbatim property value in exception messages. There are six instances:
Instance 1 -- loadBoolean() (lines 26-27):
throw new PrometheusPropertiesException(
String.format("%s: Expecting 'true' or 'false'. Found: %s", fullKey, property));Instance 2 -- loadDoubleList() (lines 90-91):
throw new PrometheusPropertiesException(
fullKey + "=" + property + ": Expecting comma separated list of double values");Instance 3 -- loadInteger() (lines 132-133):
throw new PrometheusPropertiesException(
fullKey + "=" + property + ": Expecting integer value");Instance 4 -- loadDouble() (lines 148-149):
throw new PrometheusPropertiesException(
fullKey + "=" + property + ": Expecting double value");Instance 5 -- loadLong() (lines 164-165):
throw new PrometheusPropertiesException(
fullKey + "=" + property + ": Expecting long value");Instance 6 -- assertValue() (lines 196-197):
String fullMessage = String.format("%s: %s Found: %s", fullKey, message, number);
throw new PrometheusPropertiesException(fullMessage);In every case, the raw, unredacted value of property is interpolated directly into the exception message string.
4.2 Exception Type: Unchecked RuntimeException
File: prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesException.java
@StableApi
public class PrometheusPropertiesException extends RuntimeException {
private static final long serialVersionUID = 0L;
public PrometheusPropertiesException(String msg) {
super(msg);
}
public PrometheusPropertiesException(String msg, Exception cause) {
super(msg, cause);
}
}Because PrometheusPropertiesException extends RuntimeException, it is not required to be declared in method signatures and is not required to be caught. This allows it to propagate unchecked through the entire call stack to the HTTP handler layer.
4.3 Propagation Path: PrometheusScrapeHandler
File: prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java (lines 99-105)
public void handleRequest(PrometheusHttpExchange exchange) throws IOException {
try {
// ... scrape metrics ...
} catch (IOException e) {
exchange.handleException(e);
} catch (RuntimeException e) {
exchange.handleException(e); // <-- PrometheusPropertiesException caught here
} finally {
exchange.close();
}
}The catch (RuntimeException e) clause catches any PrometheusPropertiesException and forwards it to the exchange adapter's handleException() method without sanitizing the exception message.
4.4 Disclosure Vector: HTTP Response with Stack Trace
File: prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HttpExchangeAdapter.java (lines 98-114)
@Override
public void handleException(RuntimeException e) {
sendErrorResponseWithStackTrace(e);
}
private void sendErrorResponseWithStackTrace(Exception requestHandlerException) {
if (!responseSent) {
responseSent = true;
try {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
printWriter.write("An Exception occurred while scraping metrics: ");
requestHandlerException.printStackTrace(new PrintWriter(printWriter));
byte[] stackTrace = stringWriter.toString().getBytes(StandardCharsets.UTF_8);
httpExchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
httpExchange.sendResponseHeaders(500, stackTrace.length);
httpExchange.getResponseBody().write(stackTrace);
} catch (IOException errorWriterException) {
// ...
}
}
}The requestHandlerException.printStackTrace() call serializes the full exception chain -- including the PrometheusPropertiesException message containing the raw property value -- into a StringWriter, which is then written verbatim to the HTTP response body.
4.5 Call Sites: Properties Loaded During Initialization
File: prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterHttpServerProperties.java (lines 34-44)
static ExporterHttpServerProperties load(PropertySource propertySource)
throws PrometheusPropertiesException {
Integer port = Util.loadInteger(PREFIX, PORT, propertySource); // <-- can throw with raw value
Util.assertValue(port, t -> t > 0, "Expecting value > 0.", PREFIX, PORT);
Boolean preferUncompressedResponse =
Util.loadBoolean(PREFIX, PREFER_UNCOMPRESSED_RESPONSE, propertySource);
return new ExporterHttpServerProperties(
port, preferUncompressedResponse != null && preferUncompressedResponse);
}The Util.loadInteger() call parses the io.prometheus.exporter.http_server.port property. If the value is not a valid integer, the exception message includes the raw value.
5. CVSS 3.1 Detailed Breakdown
| Metric | Value | Justification |
|---|---|---|
| Attack Vector (AV) | Network (N) | The /metrics HTTP endpoint is network-accessible. The HTTPServer binds to a network socket (default: all interfaces). |
| Attack Complexity (AC) | Low (L) | No special conditions required beyond network access to the metrics port. The attacker only needs to send a standard HTTP request to the endpoint during a configuration error condition. |
| Privileges Required (PR) | None (N) | The HTTPServer does not require authentication by default. The Authenticator is an optional builder parameter that is null unless explicitly configured. |
| User Interaction (UI) | None (N) | No user interaction is needed. The attacker sends an HTTP request directly to the endpoint. |
| Scope (S) | Unchanged (U) | The vulnerability affects only the confidentiality of the vulnerable component; it does not cross trust boundaries to impact other components. |
| Confidentiality (C) | Low (L) | The disclosed information consists of raw configuration property values. If those values happen to contain credentials, API keys, or tokens (due to misconfiguration), the impact escalates, but the base vulnerability provides limited information. |
| Integrity (I) | None (N) | No data modification occurs. |
| Availability (A) | None (N) | No denial-of-service impact. |
CVSS 3.1 Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
CVSS 3.1 Base Score: 5.3 (Medium)
6. Attack Chain Analysis
6.1 Primary Attack Chain: Lazy Servlet Initialization
The most realistic exploitation scenario involves servlet-based deployments where initialization is deferred until the first HTTP request:
- A web application registers
PrometheusMetricsServletwithload-on-startupunset or set to a negative value. - The servlet container defers servlet initialization until the first request to the servlet's URL pattern.
- An environment variable or system property intended for a different purpose (e.g.,
IO_PROMETHEUS_EXPORTER_HTTP_SERVER_PORT) is set to a non-numeric sensitive value. - On the first request, the servlet container calls
PrometheusMetricsServlet()constructor, which callsPrometheusProperties.get(), triggeringPrometheusPropertiesLoader.load(). ExporterHttpServerProperties.load()callsUtil.loadInteger(), which throwsPrometheusPropertiesExceptionwith the raw value in the message.- For the httpserver module,
HttpExchangeAdapter.sendErrorResponseWithStackTrace()writes the full exception to the HTTP response. For servlet modules, the exception propagates to the container, which may display it in its default error page.
6.2 Secondary Attack Chain: Static Initializer Wrapping
When PrometheusProperties class initialization fails:
PrometheusProperties.instanceis initialized via static field initializer:PrometheusPropertiesLoader.load().- If
load()throwsPrometheusPropertiesException, the JVM wraps it inExceptionInInitializerError. - The original
PrometheusPropertiesException(containing the raw property value) is preserved as the cause. - When
printStackTrace()is called, the full cause chain is printed, including thePrometheusPropertiesExceptionmessage with the raw value.
6.3 Tertiary Attack Chain: Custom Collectors Triggering Config Reload
If application code calls PrometheusProperties.get() or accesses configuration lazily within a custom Collector.collect() method, any resulting PrometheusPropertiesException will propagate through PrometheusScrapeHandler.handleRequest() to HttpExchangeAdapter.sendErrorResponseWithStackTrace(), exposing the raw property value in the HTTP response.
7. Proof of Concept
7.1 Prerequisites
- A Java application using
prometheus-metrics-exporter-httpserverwith the built-inHTTPServer - Network access to the metrics HTTP port
7.2 Reproduction Steps
Step 1: Set a configuration property to a value that will fail type parsing.
The value should represent sensitive data that an operator might accidentally place in a Prometheus configuration property. For example, set the histogram upper bounds to a value containing a credential:
# Simulates an operator who accidentally assigns a database password
# to a numeric configuration property
export IO_PROMETHEUS_METRICS_HISTOGRAM_CLASSIC_UPPER_BOUNDS="db_pass:S3cretK3y!@host:5432"Alternatively, to target the HTTP server port property:
export IO_PROMETHEUS_EXPORTER_HTTP_SERVER_PORT="my_secret_api_key_12345"Step 2: Start the application with a Prometheus HTTP server.
import io.prometheus.metrics.core.metrics.Histogram;
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
public class VulnerableApp {
public static void main(String[] args) throws Exception {
// Register a histogram to ensure histogram config properties are read
Histogram histogram = Histogram.builder()
.name("example_histogram")
.help("An example histogram")
.register();
HTTPServer server = HTTPServer.builder()
.port(9400)
.buildAndStart();
System.out.println("Server started on port " + server.getPort());
Thread.currentThread().join();
}
}Step 3: Request the metrics endpoint.
curl -s http://localhost:9400/metricsStep 4: Observe the raw property value in the HTTP response.
Expected HTTP 500 response body:
An Exception occurred while scraping metrics: io.prometheus.metrics.config.PrometheusPropertiesException:
io.prometheus.metrics.histogram.classic_upper_bounds=db_pass:S3cretK3y!@host:5432: Expecting comma separated list of double values
at io.prometheus.metrics.config.Util.loadDoubleList(Util.java:90)
at io.prometheus.metrics.config.MetricsProperties.load(MetricsProperties.java:...)
at io.prometheus.metrics.config.PrometheusPropertiesLoader.load(PrometheusPropertiesLoader.java:25)
at io.prometheus.metrics.config.PrometheusProperties.<clinit>(PrometheusProperties.java:17)
at io.prometheus.metrics.exporter.common.PrometheusScrapeHandler.<init>(PrometheusScrapeHandler.java:37)
at io.prometheus.metrics.exporter.httpserver.MetricsHandler.<init>(MetricsHandler.java:18)
at io.prometheus.metrics.exporter.httpserver.HTTPServer.<init>(HTTPServer.java:88)
...
The raw value db_pass:S3cretK3y!@host:5432 is fully visible in both the exception message and the HTTP response body.
7.3 Servlet-Based Reproduction
For servlet containers with lazy initialization:
web.xml (no load-on-startup):
<servlet>
<servlet-name>prometheus</servlet-name>
<servlet-class>
io.prometheus.metrics.exporter.servlet.jakarta.PrometheusMetricsServlet
</servlet-class>
<!-- No load-on-startup: servlet initialized on first request -->
</servlet>
<servlet-mapping>
<servlet-name>prometheus</servlet-name>
<url-pattern>/metrics</url-pattern>
</servlet-mapping>Step 1: Set an invalid property with a sensitive value.
export IO_PROMETHEUS_EXPORTER_HTTP_SERVER_PORT="Bearer_eyJhbGciOiJIUzI1NiJ9.secret"Step 2: Deploy the application and send the first request.
curl -s http://localhost:8080/metricsStep 3: The servlet container receives the PrometheusPropertiesException.
The Jakarta and Javax servlet adapters re-throw the exception (throw e; in handleException()), delegating error handling to the servlet container. Many servlet containers (Tomcat, Jetty) include the exception message in their default error page, disclosing the raw property value.
7.4 Integer Port Property Example
# Set port to a value containing an OAuth token
export IO_PROMETHEUS_EXPORTER_HTTP_SERVER_PORT="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"When ExporterHttpServerProperties.load() calls Util.loadInteger(), the thrown exception message will be:
io.prometheus.exporter.http_server.port=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: Expecting integer value
This message, containing the full GitHub Personal Access Token, is then written to the HTTP response body via sendErrorResponseWithStackTrace().
8. Impact Analysis
8.1 Direct Impact: Sensitive Configuration Value Disclosure
Raw configuration property values are exposed to unauthenticated HTTP clients. The severity depends on what those values contain:
| Scenario | Leaked Data | Severity |
|---|---|---|
| Database password in wrong property | Full connection string/password | Critical |
| API key/token in wrong property | Full API key or bearer token | Critical |
| Internal URL with embedded credentials | Host, port, username, password | High |
| Non-sensitive misconfiguration | Configuration value | Low |
8.2 Root Cause vs. Symptom Distinction
This vulnerability is distinct from the general stack trace disclosure issue (CVE Report #2). While stack trace disclosure reveals internal implementation details (class names, method names, line numbers), this vulnerability specifically leaks the content of configuration property values. The distinction matters because:
- Stack traces reveal structural information about the application (reconnaissance value).
- Property values may contain credentials, tokens, or secrets (direct exploitation value).
The two vulnerabilities compound: the stack trace provides the delivery mechanism, and the raw property value in the exception message provides the sensitive payload.
8.3 Why Misconfiguration Happens
Configuration errors that trigger this vulnerability are not hypothetical:
- Environment variable collisions: In containerized environments, environment variables from different services may overlap. An environment variable like
IO_PROMETHEUS_EXPORTER_HTTP_SERVER_PORTmight be set by an orchestration tool to a value containing metadata or credentials. - Copy-paste errors: Operators copying configuration blocks may accidentally assign a credential value to a Prometheus property.
- Configuration management tools: Templating errors in tools like Ansible, Terraform, or Helm can substitute incorrect values into property placeholders.
- Twelve-factor app patterns: Applications following the twelve-factor methodology store all configuration in environment variables, increasing the chance of sensitive values being present in the environment.
8.4 Deployment Context
The HTTPServer component is commonly used in:
- Standalone Java applications without a servlet container
- Java agents for JVM instrumentation
- Microservices exposing metrics alongside application traffic
In Kubernetes deployments, the metrics port is frequently reachable from any pod in the cluster via the pod IP or service IP, and Prometheus scrape targets are often discoverable through service discovery. An attacker with network access to the cluster can enumerate and scrape all metrics endpoints.
9. Scope Clarification
Affected Modules
| Module | Role | Impact |
|---|---|---|
prometheus-metrics-config |
Root cause -- raw values embedded in exception messages | All 6 throw sites in Util.java include verbatim property values |
prometheus-metrics-exporter-httpserver |
Direct disclosure vector -- sendErrorResponseWithStackTrace() writes full exception to HTTP response |
Raw values visible in HTTP 500 response body |
prometheus-metrics-exporter-servlet-jakarta |
Indirect disclosure vector -- handleException() re-throws to servlet container |
Raw values may appear in container error pages (depends on container configuration) |
prometheus-metrics-exporter-servlet-javax |
Indirect disclosure vector -- handleException() re-throws to servlet container |
Same as Jakarta variant |
Comparison: httpserver vs. Servlet Adapters
| Behavior | httpserver | servlet-jakarta / servlet-javax |
|---|---|---|
handleException(RuntimeException e) |
Calls sendErrorResponseWithStackTrace(e) -- writes full stack trace to response |
throw e; -- re-throws to servlet container |
| Disclosure guaranteed? | Yes -- always writes stack trace to response body | Depends on container error page configuration |
| Authentication by default? | None (optional Authenticator) |
Depends on servlet container and web.xml security constraints |
10. Recommended Remediation
10.1 Primary Fix: Redact Property Values in Exception Messages (Util.java)
Replace raw property value interpolation with a redacted form that indicates the value type and length without disclosing its content:
// BEFORE (vulnerable):
throw new PrometheusPropertiesException(
fullKey + "=" + property + ": Expecting integer value");
// AFTER (remediated):
throw new PrometheusPropertiesException(
fullKey + "=<redacted " + property.length() + " chars>: Expecting integer value");Apply this pattern to all six throw sites in Util.java:
loadBoolean()line 27loadDoubleList()line 91loadInteger()line 133loadDouble()line 149loadLong()line 165assertValue()line 197
10.2 Secondary Fix: Sanitize Error Responses in HttpExchangeAdapter
In HttpExchangeAdapter.sendErrorResponseWithStackTrace(), replace the full stack trace with a generic error message and log the details server-side:
private void sendErrorResponseWithStackTrace(Exception requestHandlerException) {
if (!responseSent) {
responseSent = true;
try {
// Log full details server-side only
Logger.getLogger(this.getClass().getName())
.log(Level.SEVERE,
"An Exception occurred while scraping metrics.",
requestHandlerException);
// Send generic message to client -- no exception details
byte[] message = "An internal error occurred while scraping metrics.\n"
.getBytes(StandardCharsets.UTF_8);
httpExchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
httpExchange.sendResponseHeaders(500, message.length);
httpExchange.getResponseBody().write(message);
} catch (IOException errorWriterException) {
// ... existing fallback logging ...
}
}
}10.3 Structural Fix: Fail-Fast at Startup
Move all configuration validation to application startup time rather than allowing lazy initialization to defer errors to scrape time:
- Call
PrometheusProperties.get()eagerly duringHTTPServer.builder()(already done) and during servletinit(). - Validate all properties during
PrometheusPropertiesLoader.load()(already done for known properties). - Document that applications should call
PrometheusProperties.get()during startup to surface configuration errors early.
10.4 Defense in Depth: Make PrometheusPropertiesException Checked
Consider changing PrometheusPropertiesException to extend Exception instead of RuntimeException. This would force all callers to explicitly handle or declare the exception, preventing accidental propagation to HTTP clients. Note: this is a breaking API change and would require a major version bump.
10.5 Remediation Priority
| Fix | Priority | Breaking Change | Effort |
|---|---|---|---|
| 10.1 Redact values in exception messages | P0 -- Critical | No | Low |
| 10.2 Sanitize HTTP error responses | P0 -- Critical | No | Low |
| 10.3 Fail-fast at startup | P1 -- High | No | Medium |
| 10.4 Make exception checked | P2 -- Medium | Yes | High |
Fixes 10.1 and 10.2 provide independent layers of defense and should both be applied. Fix 10.1 eliminates the root cause (sensitive values in exception messages). Fix 10.2 eliminates the disclosure vector (stack traces in HTTP responses). Together they provide defense in depth.
11. References
- CWE-209: Generation of Error Message Containing Sensitive Information
- OWASP: Improper Error Handling
- OWASP Testing Guide: Testing for Stack Traces (OTG-ERR-002)
- Source Repository: prometheus/client_java
- Vulnerable File (root cause):
Util.java - Vulnerable File (disclosure vector):
HttpExchangeAdapter.java - Exception type:
PrometheusPropertiesException.java - Scrape handler:
PrometheusScrapeHandler.java
Activity
fix: redact sensitive error values
fix: redact sensitive error values
manqingzhou commentedon Jul 15, 2026
@dhoard can i get a cve for this vulnerability and get credited as sherry zhou?
dhoard commentedon Jul 17, 2026
@manqingzhou Thank you for reporting the issue.
Per team discussion, based on the Prometheus Security model (https://prometheus.io/docs/operating/security/), this issue does not warrant issuing a CVE notice.