Swift Prometheus un-sanitized metric name or labels can be used to take over exported metrics
Description
Unsanitized user input in metric names or labels in Swift Prometheus allows attackers to inject newlines and braces, enabling arbitrary metric creation and potential denial-of-service.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Unsanitized user input in metric names or labels in Swift Prometheus allows attackers to inject newlines and braces, enabling arbitrary metric creation and potential denial-of-service.
The vulnerability in Swift Prometheus (a Swift client for Prometheus) arises from improper sanitization of user-supplied string values when constructing metric names or labels. An attacker can inject newlines, closing braces }, or similar characters via input such as a ?lang query parameter, causing the exported Prometheus format to be altered [1][3].
To exploit, the application must use unsanitized input directly as metric labels or names. No authentication is required; the attacker simply sends a crafted HTTP request. The lack of input validation allows the attacker to break out of the intended metric structure [3].
Impact includes the ability to create an unbounded number of stored metrics, leading to inflated server memory usage and potential denial-of-service. Additionally, "bogus" metrics can be generated, disrupting monitoring and alerting [1][3].
The fix is included in version 2.0.0-alpha.2, which introduces validation of metric names and label names against allowed patterns ([a-zA-Z_:][a-zA-Z0-9_:]* for names, [a-zA-Z_][a-zA-Z0-9_]* for label keys). Label values are not automatically sanitized, so developers must still validate them [2][3]. Users should upgrade immediately and avoid using unsanitized user input for metric labels.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/swift-server/swift-prometheusSwiftURL | >= 2.0.0-alpha.1, < 2.0.0-alpha.2 | 2.0.0-alpha.2 |
Affected products
2- ghsa-coordsRange: >= 2.0.0-alpha.1, < 2.0.0-alpha.2
- swift-server/swift-prometheusv5Range: = 2.0.0-alpha.1
Patches
1bfcd4bbfabe1Merge pull request from GHSA-x768-cvr2-345r
2 files changed · +211 −4
Sources/Prometheus/PrometheusCollectorRegistry.swift+116 −4 modified@@ -73,7 +73,8 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter name: A name to identify ``Counter``'s value. /// - Returns: A ``Counter`` that is registered with this ``PrometheusCollectorRegistry`` public func makeCounter(name: String) -> Counter { - self.box.withLockedValue { store -> Counter in + let name = name.ensureValidMetricName() + return self.box.withLockedValue { store -> Counter in guard let value = store[name] else { let counter = Counter(name: name, labels: []) store[name] = .counter(counter) @@ -106,6 +107,9 @@ public final class PrometheusCollectorRegistry: Sendable { return self.makeCounter(name: name) } + let name = name.ensureValidMetricName() + let labels = labels.ensureValidLabelNames() + return self.box.withLockedValue { store -> Counter in guard let value = store[name] else { let labelNames = labels.allLabelNames @@ -154,7 +158,8 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter name: A name to identify ``Gauge``'s value. /// - Returns: A ``Gauge`` that is registered with this ``PrometheusCollectorRegistry`` public func makeGauge(name: String) -> Gauge { - self.box.withLockedValue { store -> Gauge in + let name = name.ensureValidMetricName() + return self.box.withLockedValue { store -> Gauge in guard let value = store[name] else { let gauge = Gauge(name: name, labels: []) store[name] = .gauge(gauge) @@ -187,6 +192,9 @@ public final class PrometheusCollectorRegistry: Sendable { return self.makeGauge(name: name) } + let name = name.ensureValidMetricName() + let labels = labels.ensureValidLabelNames() + return self.box.withLockedValue { store -> Gauge in guard let value = store[name] else { let labelNames = labels.allLabelNames @@ -236,7 +244,8 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter buckets: Define the buckets that shall be used within the ``DurationHistogram`` /// - Returns: A ``DurationHistogram`` that is registered with this ``PrometheusCollectorRegistry`` public func makeDurationHistogram(name: String, buckets: [Duration]) -> DurationHistogram { - self.box.withLockedValue { store -> DurationHistogram in + let name = name.ensureValidMetricName() + return self.box.withLockedValue { store -> DurationHistogram in guard let value = store[name] else { let gauge = DurationHistogram(name: name, labels: [], buckets: buckets) store[name] = .durationHistogram(gauge) @@ -274,6 +283,9 @@ public final class PrometheusCollectorRegistry: Sendable { return self.makeDurationHistogram(name: name, buckets: buckets) } + let name = name.ensureValidMetricName() + let labels = labels.ensureValidLabelNames() + return self.box.withLockedValue { store -> DurationHistogram in guard let value = store[name] else { let labelNames = labels.allLabelNames @@ -335,7 +347,8 @@ public final class PrometheusCollectorRegistry: Sendable { /// - Parameter buckets: Define the buckets that shall be used within the ``ValueHistogram`` /// - Returns: A ``ValueHistogram`` that is registered with this ``PrometheusCollectorRegistry`` public func makeValueHistogram(name: String, buckets: [Double]) -> ValueHistogram { - self.box.withLockedValue { store -> ValueHistogram in + let name = name.ensureValidMetricName() + return self.box.withLockedValue { store -> ValueHistogram in guard let value = store[name] else { let gauge = ValueHistogram(name: name, labels: [], buckets: buckets) store[name] = .valueHistogram(gauge) @@ -364,6 +377,9 @@ public final class PrometheusCollectorRegistry: Sendable { return self.makeValueHistogram(name: name, buckets: buckets) } + let name = name.ensureValidMetricName() + let labels = labels.ensureValidLabelNames() + return self.box.withLockedValue { store -> ValueHistogram in guard let value = store[name] else { let labelNames = labels.allLabelNames @@ -560,6 +576,14 @@ extension [(String, String)] { result = result.sorted() return result } + + fileprivate func ensureValidLabelNames() -> [(String, String)] { + if self.allSatisfy({ $0.0.isValidLabelName() }) { + return self + } else { + return self.map { ($0.ensureValidLabelName(), $1) } + } + } } extension [UInt8] { @@ -595,3 +619,91 @@ extension PrometheusMetric { return prerendered } } + +extension String { + fileprivate func isValidMetricName() -> Bool { + var isFirstCharacter = true + for ascii in self.utf8 { + defer { isFirstCharacter = false } + switch ascii { + case UInt8(ascii: "A")...UInt8(ascii: "Z"), + UInt8(ascii: "a")...UInt8(ascii: "z"), + UInt8(ascii: "_"), UInt8(ascii: ":"): + continue + case UInt8(ascii: "0"), UInt8(ascii: "9"): + if isFirstCharacter { + return false + } + continue + default: + return false + } + } + return true + } + + fileprivate func isValidLabelName() -> Bool { + var isFirstCharacter = true + for ascii in self.utf8 { + defer { isFirstCharacter = false } + switch ascii { + case UInt8(ascii: "A")...UInt8(ascii: "Z"), + UInt8(ascii: "a")...UInt8(ascii: "z"), + UInt8(ascii: "_"): + continue + case UInt8(ascii: "0"), UInt8(ascii: "9"): + if isFirstCharacter { + return false + } + continue + default: + return false + } + } + return true + } + + fileprivate func ensureValidMetricName() -> String { + if self.isValidMetricName() { + return self + } else { + var new = self + new.fixPrometheusName(allowColon: true) + return new + } + } + + fileprivate func ensureValidLabelName() -> String { + if self.isValidLabelName() { + return self + } else { + var new = self + new.fixPrometheusName(allowColon: false) + return new + } + } + + fileprivate mutating func fixPrometheusName(allowColon: Bool) { + var startIndex = self.startIndex + var isFirstCharacter = true + while let fixIndex = self[startIndex...].firstIndex(where: { character in + defer { isFirstCharacter = false } + switch character { + case "A"..."Z", "a"..."z", "_": + return false + case ":": + return !allowColon + case "0"..."9": + return isFirstCharacter + default: + return true + } + }) { + self.replaceSubrange(fixIndex...fixIndex, with: CollectionOfOne("_")) + startIndex = fixIndex + if startIndex == self.endIndex { + break + } + } + } +}
Tests/PrometheusTests/ValidNamesTests.swift+95 −0 added@@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftPrometheus open source project +// +// Copyright (c) 2024 SwiftPrometheus project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftPrometheus project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Prometheus +import XCTest + +final class ValidNamesTests: XCTestCase { + func testCounterWithEmoji() { + let client = PrometheusCollectorRegistry() + let counter = client.makeCounter(name: "coffee☕️", labels: []) + counter.increment() + + var buffer = [UInt8]() + client.emit(into: &buffer) + XCTAssertEqual( + String(decoding: buffer, as: Unicode.UTF8.self), + """ + # TYPE coffee_ counter + coffee_ 1 + + """ + ) + } + + func testIllegalMetricNames() async throws { + let registry = PrometheusCollectorRegistry() + + /// Notably, newlines must not allow creating whole new metric root + let tests = [ + "name", + """ + name{bad="haha"} 121212121 + bad_bad 12321323 + """ + ] + + for test in tests { + registry.makeCounter( + name: test, + labels: [] + ).increment() + } + + var buffer = [UInt8]() + registry.emit(into: &buffer) + XCTAssertEqual( + String(decoding: buffer, as: Unicode.UTF8.self).split(separator: "\n").sorted().joined(separator: "\n"), + """ + # TYPE name counter + # TYPE name_bad__haha___121212121_bad_bad_12321323 counter + name 1 + name_bad__haha___121212121_bad_bad_12321323 1 + """ + ) + } + + func testIllegalLabelNames() async throws { + let registry = PrometheusCollectorRegistry() + + let tests = [ + """ + name{bad="haha"} 121212121 + bad_bad 12321323 + """ + ] + + for test in tests { + registry.makeCounter( + name: "metric", + labels: [(test, "value")] + ).increment() + } + + var buffer = [UInt8]() + registry.emit(into: &buffer) + XCTAssertEqual( + String(decoding: buffer, as: Unicode.UTF8.self).split(separator: "\n").sorted().joined(separator: "\n"), + """ + # TYPE metric counter + metric{name_bad__haha___121212121_bad_bad_12321323="value"} 1 + """ + ) + } +}
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-x768-cvr2-345rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-28867ghsaADVISORY
- github.com/swift-server/swift-prometheus/commit/bfcd4bbfabe11aae4b035424ee9724582e288501ghsax_refsource_MISCWEB
- github.com/swift-server/swift-prometheus/security/advisories/GHSA-x768-cvr2-345rghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.