diff --git a/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequest.java b/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequest.java index a0c692c23..4f051f82c 100644 --- a/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequest.java +++ b/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequest.java @@ -49,15 +49,42 @@ default String getParameter(String name) { @SuppressWarnings("JdkObsolete") default String[] getParameterValues(String name) { try { + int maxQueryStringLength = 64 * 1024; + int maxQueryParameterCount = 1024; ArrayList result = new ArrayList<>(); String queryString = getQueryString(); if (queryString != null) { - String[] pairs = queryString.split("&"); - for (String pair : pairs) { + if (queryString.length() > maxQueryStringLength) { + throw new IllegalArgumentException( + "Query string too long: " + + queryString.length() + + " characters (max " + + maxQueryStringLength + + ")"); + } + int start = 0; + int parameterCount = 0; + for (int end = queryString.indexOf('&'); start <= queryString.length(); ) { + parameterCount++; + if (parameterCount > maxQueryParameterCount) { + throw new IllegalArgumentException( + "Too many query parameters: " + + parameterCount + + " (max " + + maxQueryParameterCount + + ")"); + } + String pair = + end == -1 ? queryString.substring(start) : queryString.substring(start, end); 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 (end == -1) { + break; + } + start = end + 1; + end = queryString.indexOf('&', start); } } if (result.isEmpty()) { diff --git a/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java b/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java index 20328382a..238efd58a 100644 --- a/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java +++ b/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java @@ -96,6 +96,13 @@ public void handleRequest(PrometheusHttpExchange exchange) throws IOException { } } } + } catch (IllegalArgumentException e) { + PrometheusHttpResponse response = exchange.getResponse(); + response.setHeader("Content-Type", "text/plain; charset=utf-8"); + byte[] message = "Invalid query parameters".getBytes(StandardCharsets.UTF_8); + try (OutputStream outputStream = response.sendHeadersAndGetBody(400, message.length)) { + outputStream.write(message); + } } catch (IOException e) { exchange.handleException(e); } catch (RuntimeException e) { diff --git a/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java b/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java index 07ea8ac52..4f190b61f 100644 --- a/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java +++ b/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java @@ -164,6 +164,36 @@ void testMultipleMetricNameFilters() throws IOException { assertThat(body).doesNotContain("metric_three"); } + @Test + void testRejectsTooManyQueryParameters() throws IOException { + StringBuilder queryString = new StringBuilder("name[]=test_counter"); + for (int i = 0; i < 1024; i++) { + queryString.append("&name[]=metric_").append(i); + } + + TestHttpExchange exchange = new TestHttpExchange("GET", queryString.toString()); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(400); + assertThat(exchange.getResponseHeaders().get("Content-Type")) + .isEqualTo("text/plain; charset=utf-8"); + assertThat(exchange.getResponseBody()).isEqualTo("Invalid query parameters"); + } + + @Test + void testRejectsTooLongQueryString() throws IOException { + StringBuilder queryString = new StringBuilder("name[]="); + for (int i = 0; i < 64 * 1024; i++) { + queryString.append("a"); + } + + TestHttpExchange exchange = new TestHttpExchange("GET", queryString.toString()); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(400); + assertThat(exchange.getResponseBody()).isEqualTo("Invalid query parameters"); + } + /** Test implementation of PrometheusHttpExchange for testing. */ private static class TestHttpExchange implements PrometheusHttpExchange { private final TestHttpRequest request; @@ -216,7 +246,7 @@ public Map getResponseHeaders() { } public String getResponseBody() { - return rawResponseBody.toString(StandardCharsets.UTF_8); + return new String(rawResponseBody.toByteArray(), StandardCharsets.UTF_8); } public boolean isGzipCompressed() { diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MetricNameFilter.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MetricNameFilter.java index 642f27150..ded171500 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MetricNameFilter.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MetricNameFilter.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.function.Predicate; import javax.annotation.Nullable; @@ -26,10 +27,10 @@ private MetricNameFilter( Collection nameIsNotEqualTo, Collection nameStartsWith, Collection nameDoesNotStartWith) { - this.nameIsEqualTo = unmodifiableCollection(new ArrayList<>(nameIsEqualTo)); - this.nameIsNotEqualTo = unmodifiableCollection(new ArrayList<>(nameIsNotEqualTo)); - this.nameStartsWith = unmodifiableCollection(new ArrayList<>(nameStartsWith)); - this.nameDoesNotStartWith = unmodifiableCollection(new ArrayList<>(nameDoesNotStartWith)); + this.nameIsEqualTo = unmodifiableCollection(new LinkedHashSet<>(nameIsEqualTo)); + this.nameIsNotEqualTo = unmodifiableCollection(new LinkedHashSet<>(nameIsNotEqualTo)); + this.nameStartsWith = unmodifiableCollection(new LinkedHashSet<>(nameStartsWith)); + this.nameDoesNotStartWith = unmodifiableCollection(new LinkedHashSet<>(nameDoesNotStartWith)); } @Override @@ -44,6 +45,9 @@ private boolean matchesNameEqualTo(String metricName) { if (nameIsEqualTo.isEmpty()) { return true; } + if (nameIsEqualTo.contains(metricName)) { + 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". @@ -58,6 +62,9 @@ private boolean matchesNameNotEqualTo(String metricName) { if (nameIsNotEqualTo.isEmpty()) { return false; } + if (nameIsNotEqualTo.contains(metricName)) { + return true; + } for (String name : nameIsNotEqualTo) { // The following ignores suffixes like _total. // "request_count" and "request_count_total" both match a metric named "request_count".