From 980b6cfd88af99caaa6cab69755ee05a1df91095 Mon Sep 17 00:00:00 2001 From: Christopher Krah <48338417+chrikrah@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:08:09 +0200 Subject: [PATCH] Reject an empty label name under UTF-8 validation With legacy validation off, which is the default, an empty string passes _validate_labelname. A metric declared with labelnames=[''] then exposes g{="x"} 1.0, which both bundled parsers reject as invalid labels. Legacy validation already refuses it, and _validate_metric_name refuses an empty metric name the same way. Both parsers call the same function, so a quoted empty label name such as a{""="x"} 1 is now refused on input as well. Signed-off-by: Christopher Krah <48338417+chrikrah@users.noreply.github.com> --- prometheus_client/validation.py | 2 ++ tests/openmetrics/test_parser.py | 1 + tests/test_core.py | 15 +++++++++++++++ tests/test_parser.py | 8 ++++++++ 4 files changed, 26 insertions(+) diff --git a/prometheus_client/validation.py b/prometheus_client/validation.py index cdbba116..a7590a17 100644 --- a/prometheus_client/validation.py +++ b/prometheus_client/validation.py @@ -78,6 +78,8 @@ def _validate_labelname(l): This check uses the global legacy validation setting to determine the validation scheme. """ + if not l: + raise ValueError('label name cannot be empty') if get_legacy_validation(): if not METRIC_LABEL_NAME_RE.match(l): raise ValueError('Invalid label metric name: ' + l) diff --git a/tests/openmetrics/test_parser.py b/tests/openmetrics/test_parser.py index 79a2158c..039e1f84 100644 --- a/tests/openmetrics/test_parser.py +++ b/tests/openmetrics/test_parser.py @@ -774,6 +774,7 @@ def test_invalid_input(self): ('a{a=" # "}1\n# EOF\n'), ('a{a=" # ",b=}1\n# EOF\n'), ('a{a=" # "b}1\n# EOF\n'), + ('a{""="1"} 1\n# EOF\n'), # Missing value. ('a\n# EOF\n'), ('a \n# EOF\n'), diff --git a/tests/test_core.py b/tests/test_core.py index 2c29f79a..25f90e6b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -818,6 +818,21 @@ def test_duplicate_labelnames_error_lists_only_duplicates(self): self.assertEqual(str(error.exception), f'Duplicate label names: {expected!r}') self.assertEqual(list(registry.collect()), []) + def test_empty_labelname_raises(self): + # The exposition would contain {="value"}, which no parser accepts. + for metric_type in (Counter, Gauge, Summary, Histogram, Info, Enum): + with self.subTest(metric_type=metric_type): + registry = CollectorRegistry() + kwargs = {'states': ['ready']} if metric_type is Enum else {} + with self.assertRaisesRegex(ValueError, '^label name cannot be empty$'): + metric_type('empty_label', 'help', labelnames=[''], registry=registry, **kwargs) + self.assertEqual(list(registry.collect()), []) + + def test_empty_exemplar_labelname_raises(self): + counter = Counter('empty_exemplar', 'help', registry=self.registry) + with self.assertRaisesRegex(ValueError, '^label name cannot be empty$'): + counter.inc(exemplar={'': 'abc'}) + def test_unique_labelnames_iterator(self): # Valid one-shot label iterables still preserve both names. counter = Counter('iter_labels', 'help', iter(['a', 'b']), registry=self.registry) diff --git a/tests/test_parser.py b/tests/test_parser.py index 8436dd0c..47b48175 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -217,6 +217,14 @@ def test_multiple_trailing_commas(self): text = """# TYPE a counter # HELP a help a{foo="bar",, } 1 +""" + self.assertRaises(ValueError, + lambda: list(text_string_to_metric_families(text))) + + def test_quoted_empty_label_name(self): + text = """# TYPE a counter +# HELP a help +a{""="bar"} 1 """ self.assertRaises(ValueError, lambda: list(text_string_to_metric_families(text)))