From 6f53cec0a374ea6e89e25d713ddaf99dfa78f5bf Mon Sep 17 00:00:00 2001 From: Sajan Ghimire Date: Tue, 26 May 2026 05:56:10 +0545 Subject: [PATCH 1/2] fix: truncate baggage/tag values to prevent metrics cardinality explosion Baggage header values written to metrics dimensions without length validation could cause unbounded time series growth if values are externally controlled (e.g. user-supplied correlation IDs passed via W3C baggage header). Each unique value creates a permanent new time series in the metrics backend, causing: - Metrics cardinality explosion - Metrics backend memory exhaustion - Loss of observability (alerts stop firing) - Unexpected Azure Monitor cost spikes Fix: truncate baggage and tag object values to 256 characters before writing to metrics dimensions. Related: GHSA-rcjv-mgp8-qvmr (same class, OpenTelemetry-Go) CWE-400: Uncontrolled Resource Consumption --- .../Internal/ActivityMetricsSender.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Activities/Internal/ActivityMetricsSender.cs b/src/Activities/Internal/ActivityMetricsSender.cs index 578c121b9..72f90d816 100644 --- a/src/Activities/Internal/ActivityMetricsSender.cs +++ b/src/Activities/Internal/ActivityMetricsSender.cs @@ -23,6 +23,13 @@ internal sealed class ActivityMetricsSender : IActivitiesEventSender, IDisposabl private readonly HashSet m_customTagObjectsDimension; private readonly bool m_isSetParentNameAsDimensionEnabled; + /// + /// Maximum length for metric tag values to prevent + /// cardinality explosion via externally controlled + /// baggage or tag values. + /// + private const int MaxMetricTagValueLength = 256; + public ActivityMetricsSender( IExecutionContext executionContext, IHostEnvironment hostEnvironment, @@ -68,7 +75,11 @@ public void SendActivityMetric(Activity activity) string? baggageItem = activity.GetBaggageItem(dimension); if (!string.IsNullOrWhiteSpace(baggageItem)) { - tagList.Add(dimension, baggageItem); + // Truncate to prevent metrics cardinality explosion + // via externally controlled W3C baggage header values + tagList.Add(dimension, baggageItem.Length > MaxMetricTagValueLength + ? baggageItem[..MaxMetricTagValueLength] + : baggageItem); } } @@ -77,7 +88,12 @@ public void SendActivityMetric(Activity activity) object? tagItem = activity.GetTagItem(dimension); if (tagItem != null) { - tagList.Add(dimension, tagItem); + // Truncate string representation to prevent + // cardinality explosion via high-cardinality tag values + string tagValue = tagItem.ToString() ?? string.Empty; + tagList.Add(dimension, tagValue.Length > MaxMetricTagValueLength + ? tagValue[..MaxMetricTagValueLength] + : tagValue); } } @@ -95,4 +111,4 @@ public void SendActivityMetric(Activity activity) public void Dispose() => m_meter.Dispose(); } -} +} \ No newline at end of file From e6783829d63703a3c2bb866e96474bbb80f4b0d1 Mon Sep 17 00:00:00 2001 From: Sajan Ghimire Date: Tue, 26 May 2026 06:28:16 +0545 Subject: [PATCH 2/2] fix: address Copilot review feedback - Preserve original type for non-string tag values - Use surrogate-pair-safe string truncation - Simplify constant comment --- .../Internal/ActivityMetricsSender.cs | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Activities/Internal/ActivityMetricsSender.cs b/src/Activities/Internal/ActivityMetricsSender.cs index 72f90d816..de97d8896 100644 --- a/src/Activities/Internal/ActivityMetricsSender.cs +++ b/src/Activities/Internal/ActivityMetricsSender.cs @@ -23,11 +23,8 @@ internal sealed class ActivityMetricsSender : IActivitiesEventSender, IDisposabl private readonly HashSet m_customTagObjectsDimension; private readonly bool m_isSetParentNameAsDimensionEnabled; - /// - /// Maximum length for metric tag values to prevent - /// cardinality explosion via externally controlled - /// baggage or tag values. - /// + // Caps metric tag value length to reduce cardinality risk + // from externally controlled baggage or tag values private const int MaxMetricTagValueLength = 256; public ActivityMetricsSender( @@ -75,11 +72,7 @@ public void SendActivityMetric(Activity activity) string? baggageItem = activity.GetBaggageItem(dimension); if (!string.IsNullOrWhiteSpace(baggageItem)) { - // Truncate to prevent metrics cardinality explosion - // via externally controlled W3C baggage header values - tagList.Add(dimension, baggageItem.Length > MaxMetricTagValueLength - ? baggageItem[..MaxMetricTagValueLength] - : baggageItem); + tagList.Add(dimension, TruncateSafe(baggageItem, MaxMetricTagValueLength)); } } @@ -88,12 +81,16 @@ public void SendActivityMetric(Activity activity) object? tagItem = activity.GetTagItem(dimension); if (tagItem != null) { - // Truncate string representation to prevent - // cardinality explosion via high-cardinality tag values - string tagValue = tagItem.ToString() ?? string.Empty; - tagList.Add(dimension, tagValue.Length > MaxMetricTagValueLength - ? tagValue[..MaxMetricTagValueLength] - : tagValue); + // Only truncate string values — preserve original + // type for numeric/bool to avoid breaking exporters + if (tagItem is string stringValue) + { + tagList.Add(dimension, TruncateSafe(stringValue, MaxMetricTagValueLength)); + } + else + { + tagList.Add(dimension, tagItem); + } } } @@ -109,6 +106,19 @@ public void SendActivityMetric(Activity activity) histogram.Record(durationMs, tagList); } + // Truncates string to maxLength while avoiding + // splitting UTF-16 surrogate pairs + private static string TruncateSafe(string value, int maxLength) + { + if (value.Length <= maxLength) + return value; + + if (char.IsHighSurrogate(value[maxLength - 1])) + return value[..(maxLength - 1)]; + + return value[..maxLength]; + } + public void Dispose() => m_meter.Dispose(); } -} \ No newline at end of file +}