Resource exhaustion (DoS) / Memory safety

MEDIUM
victoriametrics/victoriametrics
Commit: 414aa7b3ab09
Affected: < 1.139.0
2026-07-09 13:43 UTC

Description

The commit introduces a per-target override for max_scrape_size via the __max_scrape_size__ label in the Prometheus scraping flow. It parses the label value using flagutil.ParseBytes and, if positive, applies it to that specific scrape target instead of the global max_scrape_size. This serves as a configurational control to mitigate resource exhaustion by limiting per-target scrape payloads, addressing a potential DoS/memory exhaustion risk. However, the change does not enforce an upper bound on the per-target value, and an attacker who can influence label values could set a very large size, potentially causing memory pressure or DoS. The tests also illustrate that invalid values are ignored, and valid values override the target's limit.

Proof of Concept

PoC: Demonstrating DoS potential via per-target max_scrape_size override Prereqs: - A VictoriaMetrics vmagent setup with a target that serves a large metrics payload. - Ability to inject per-target relabels that set __max_scrape_size__. 1) Run a metrics endpoint that emits a large number of metrics, e.g. a local Python server: python3 - << 'PY' from http.server import BaseHTTPRequestHandler, HTTPServer class MetricsHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path == '/metrics': self.send_response(200) self.end_headers() # Emit a large payload to stress memory when scraped for i in range(1000000): self.wfile.write(f"metric_{i} {i}\n".encode()) httpd = HTTPServer(("0.0.0.0", 9100), MetricsHandler) httpd.serve_forever() PY 2) Configure vmagent to scrape the endpoint and override max_scrape_size per target using a relabel rule: scrape_configs: - job_name: large static_configs: - targets: ["127.0.0.1:9100"] relabel_configs: - source_labels: [__address__] regex: 127.0.0.1:9100 target_label: __max_scrape_size__ replacement: 1024MiB 3) Run vmagent with this config and observe memory usage. The per-target max_scrape_size__ is set to 1 GiB, allowing a much larger scrape payload for this target than the global default. If the endpoint can be fed with a very large response, vmagent may allocate proportional memory to hold the scraped data, potentially leading to memory exhaustion or DoS under load. 4) Verification steps: - Monitor vmagent process memory (e.g., top/htop or pmap) before, during, and after a few scrape cycles. - If memory usage spikes significantly when the large payload is scraped, this demonstrates a potential DoS risk if per-target overrides can be manipulated by an attacker. Note: This PoC is intended for controlled testing in a safe environment. Do not run against production systems.

Commit Details

Author: JAYICE

Date: 2026-07-09 12:26 UTC

Message:

lib/promscrape: support overriding `max_scrape_size` on a per-target basis via the `__max_scrape_size__` This commit allows changing max scrape size with meta label - `__max_scrape_size__`. Fixes https://github.com/VictoriaMetrics/VictoriaMetrics/issues/11188

Triage Assessment

Vulnerability Type: Resource exhaustion (DoS) / Memory safety

Confidence: MEDIUM

Reasoning:

The commit adds per-target override for max_scrape_size via a __max_scrape_size__ label, parsing and applying it to the scrape work configuration. This provides a defense against oversized per-target scrapes that could lead to excessive memory usage or DoS conditions, addressing resource exhaustion risks. While it's a configurational enhancement, it directly mitigates a potential security risk (DoS via large scrape sizes).

Verification Assessment

Vulnerability Type: Resource exhaustion (DoS) / Memory safety

Confidence: MEDIUM

Affected Versions: < 1.139.0

Code Diff

diff --git a/lib/promscrape/config.go b/lib/promscrape/config.go index ce14a4d85bef5..a111a3500631b 100644 --- a/lib/promscrape/config.go +++ b/lib/promscrape/config.go @@ -1271,6 +1271,17 @@ func (swc *scrapeWorkConfig) getScrapeWork(target string, extraLabels, metaLabel } scrapeTimeout = d } + // Read max_scrape_size option from __max_scrape_size__ label. + targetMaxScrapeSize := swc.maxScrapeSize + if s := labels.Get("__max_scrape_size__"); len(s) > 0 { + n, err := flagutil.ParseBytes(s) + if err != nil { + return nil, fmt.Errorf("cannot parse __max_scrape_size__=%q: %w", s, err) + } + if n > 0 { + targetMaxScrapeSize = n + } + } // Read series_limit option from __series_limit__ label. // See https://docs.victoriametrics.com/victoriametrics/vmagent/#cardinality-limiter seriesLimit := swc.seriesLimit @@ -1333,7 +1344,7 @@ func (swc *scrapeWorkConfig) getScrapeWork(target string, extraLabels, metaLabel ScrapeURL: scrapeURL, ScrapeInterval: scrapeInterval, ScrapeTimeout: scrapeTimeout, - MaxScrapeSize: swc.maxScrapeSize, + MaxScrapeSize: targetMaxScrapeSize, HonorLabels: swc.honorLabels, HonorTimestamps: swc.honorTimestamps, DenyRedirects: swc.denyRedirects, diff --git a/lib/promscrape/config_test.go b/lib/promscrape/config_test.go index 0af0786a1379a..73ac545c74514 100644 --- a/lib/promscrape/config_test.go +++ b/lib/promscrape/config_test.go @@ -1153,6 +1153,57 @@ scrape_configs: }) f(` scrape_configs: +- job_name: foo + max_scrape_size: 8MiB + relabel_configs: + - source_labels: [__address__] + regex: foo1:.* + target_label: __max_scrape_size__ + replacement: 2.5MiB + - source_labels: [__address__] + regex: foo2:.* + target_label: __max_scrape_size__ + replacement: -1 + static_configs: + - targets: ["foo1:1234", "foo2:1234", "foo3:1234"] +`, []*ScrapeWork{ + { + ScrapeURL: "http://foo1:1234/metrics", + ScrapeInterval: defaultScrapeInterval, + ScrapeTimeout: defaultScrapeTimeout, + MaxScrapeSize: 2.5 * 1024 * 1024, + Labels: promutil.NewLabelsFromMap(map[string]string{ + "instance": "foo1:1234", + "job": "foo", + }), + jobNameOriginal: "foo", + }, + // invalid __max_scrape_size__ will be ignored + { + ScrapeURL: "http://foo2:1234/metrics", + ScrapeInterval: defaultScrapeInterval, + ScrapeTimeout: defaultScrapeTimeout, + MaxScrapeSize: 8 * 1024 * 1024, + Labels: promutil.NewLabelsFromMap(map[string]string{ + "instance": "foo2:1234", + "job": "foo", + }), + jobNameOriginal: "foo", + }, + { + ScrapeURL: "http://foo3:1234/metrics", + ScrapeInterval: defaultScrapeInterval, + ScrapeTimeout: defaultScrapeTimeout, + MaxScrapeSize: 8 * 1024 * 1024, + Labels: promutil.NewLabelsFromMap(map[string]string{ + "instance": "foo3:1234", + "job": "foo", + }), + jobNameOriginal: "foo", + }, + }) + f(` +scrape_configs: - job_name: foo static_configs: - targets: ["foo.bar:1234"]
← Back to Alerts View on GitHub →