From f206e2366936db2fa19ec35a1a50bfc2f5333bb8 Mon Sep 17 00:00:00 2001 From: claudiogodoy99 Date: Sun, 8 Feb 2026 22:03:38 -0300 Subject: [PATCH 1/4] feat(hosting): add url.query redaction for telemetry sensitive parameters Introduce UrlQueryRedactionOptions to redact sensitive query string values (e.g., tokens, passwords, API keys) from the url.query OpenTelemetry attribute on HTTP request activities. Configurable sensitive parameter names and placeholder text. Wired through GenericWebHostService, HostingApplication, and HostingApplicationDiagnostics. Includes unit tests. --- .../src/GenericHost/GenericWebHostService.cs | 7 +- .../src/Internal/HostingApplication.cs | 5 +- .../Internal/HostingApplicationDiagnostics.cs | 19 +- .../src/Internal/HostingTelemetryHelpers.cs | 74 ++++++++ .../src/Internal/UrlQueryRedactionOptions.cs | 47 +++++ src/Hosting/Hosting/src/Internal/WebHost.cs | 4 +- .../Hosting/src/PublicAPI.Unshipped.txt | 5 + .../HostingApplicationDiagnosticsTests.cs | 169 +++++++++++++++++- 8 files changed, 319 insertions(+), 11 deletions(-) create mode 100644 src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs diff --git a/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs index 6dcecd3d50a5..afc8ffcbb160 100644 --- a/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs +++ b/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs @@ -27,7 +27,8 @@ public GenericWebHostService(IOptions options, IEnumerable startupFilters, IConfiguration configuration, IWebHostEnvironment hostingEnvironment, - HostingMetrics hostingMetrics) + HostingMetrics hostingMetrics, + IOptions? urlQueryRedactionOptions) { Options = options.Value; Server = server; @@ -42,6 +43,7 @@ public GenericWebHostService(IOptions options, Configuration = configuration; HostingEnvironment = hostingEnvironment; HostingMetrics = hostingMetrics; + UrlQueryRedactionOptions = urlQueryRedactionOptions?.Value; } public GenericWebHostServiceOptions Options { get; } @@ -58,6 +60,7 @@ public GenericWebHostService(IOptions options, public IConfiguration Configuration { get; } public IWebHostEnvironment HostingEnvironment { get; } public HostingMetrics HostingMetrics { get; } + public UrlQueryRedactionOptions? UrlQueryRedactionOptions { get; } public async Task StartAsync(CancellationToken cancellationToken) { @@ -156,7 +159,7 @@ static string ExpandPorts(string ports, string scheme) application = ErrorPageBuilder.BuildErrorPageApplication(HostingEnvironment.ContentRootFileProvider, Logger, showDetailedErrors, ex); } - var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory, HostingEventSource.Log, HostingMetrics); + var httpApplication = new HostingApplication(application, Logger, DiagnosticListener, ActivitySource, Propagator, HttpContextFactory, HostingEventSource.Log, HostingMetrics, UrlQueryRedactionOptions); await Server.StartAsync(httpApplication, cancellationToken); HostingEventSource.Log.ServerReady(); diff --git a/src/Hosting/Hosting/src/Internal/HostingApplication.cs b/src/Hosting/Hosting/src/Internal/HostingApplication.cs index 37996905bdf1..3946c8290a0a 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplication.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplication.cs @@ -32,10 +32,11 @@ public HostingApplication( DistributedContextPropagator propagator, IHttpContextFactory httpContextFactory, HostingEventSource eventSource, - HostingMetrics metrics) + HostingMetrics metrics, + UrlQueryRedactionOptions? urlQueryRedactionOptions = null) { _application = application; - _diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource, activitySource, propagator, eventSource, metrics); + _diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource, activitySource, propagator, eventSource, metrics, urlQueryRedactionOptions); if (httpContextFactory is DefaultHttpContextFactory factory) { _defaultHttpContextFactory = factory; diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index f648a4eee1a8..ce9d0065e23e 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -34,6 +34,7 @@ internal sealed class HostingApplicationDiagnostics private readonly HostingEventSource _eventSource; private readonly HostingMetrics _metrics; private readonly ILogger _logger; + private readonly UrlQueryRedactionOptions? _urlQueryRedactionOptions; // Internal for testing purposes only internal bool SuppressActivityOpenTelemetryData { get; set; } @@ -44,7 +45,8 @@ public HostingApplicationDiagnostics( ActivitySource activitySource, DistributedContextPropagator propagator, HostingEventSource eventSource, - HostingMetrics metrics) + HostingMetrics metrics, + UrlQueryRedactionOptions? urlQueryRedactionOptions) { _logger = logger; _diagnosticListener = diagnosticListener; @@ -52,6 +54,7 @@ public HostingApplicationDiagnostics( _propagator = propagator; _eventSource = eventSource; _metrics = metrics; + _urlQueryRedactionOptions = urlQueryRedactionOptions; SuppressActivityOpenTelemetryData = GetSuppressActivityOpenTelemetryData(); } @@ -411,7 +414,7 @@ private void RecordRequestStartMetrics(HttpContext httpContext) hasDiagnosticListener = false; var initializeTags = !SuppressActivityOpenTelemetryData - ? CreateInitializeActivityTags(httpContext) + ? CreateInitializeActivityTags(httpContext, _urlQueryRedactionOptions) : (TagList?)null; var headers = httpContext.Request.Headers; @@ -457,13 +460,12 @@ private void RecordRequestStartMetrics(HttpContext httpContext) return activity; } - private static TagList CreateInitializeActivityTags(HttpContext httpContext) + private static TagList CreateInitializeActivityTags(HttpContext httpContext, UrlQueryRedactionOptions? urlQueryRedactionOptions) { // The tags here are set when the activity is created. They can be used in sampling decisions. // Most values in semantic conventions that are present at creation are specified: // https://github.com/open-telemetry/semantic-conventions/blob/27735ccca3746d7bb7fa061dfb73d93bcbae2b6e/docs/http/http-spans.md#L581-L592 // Missing values recommended by the spec are: - // - url.query (need configuration around redaction to do properly) // - http.request.header. // // Note that these tags are added even if Activity.IsAllDataRequested is false, as they may be used in sampling decisions. @@ -497,6 +499,15 @@ private static TagList CreateInitializeActivityTags(HttpContext httpContext) var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; creationTags.Add(HostingTelemetryHelpers.AttributeUrlPath, path); + if (urlQueryRedactionOptions != null && request.QueryString.HasValue) + { + var redactedQuery = HostingTelemetryHelpers.GetRedactedQueryString(request.QueryString, urlQueryRedactionOptions); + if (redactedQuery != null) + { + creationTags.Add(HostingTelemetryHelpers.AttributeUrlQuery, redactedQuery); + } + } + return creationTags; } diff --git a/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs b/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs index b172a4b74b4d..1e60f4a1653e 100644 --- a/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs +++ b/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs @@ -4,6 +4,7 @@ using System.Collections.Frozen; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Hosting; @@ -17,6 +18,7 @@ internal static class HostingTelemetryHelpers public const string AttributeHttpRoute = "http.route"; public const string AttributeUrlScheme = "url.scheme"; public const string AttributeUrlPath = "url.path"; + public const string AttributeUrlQuery = "url.query"; public const string AttributeServerAddress = "server.address"; public const string AttributeServerPort = "server.port"; public const string AttributeUserAgentOriginal = "user_agent.original"; @@ -146,4 +148,76 @@ public static string GetActivityDisplayName(string originalHttpMethod, string? h return string.IsNullOrEmpty(httpRoute) ? namePrefix : $"{namePrefix} {httpRoute}"; } + + /// + /// Redacts sensitive query parameter values from a query string. + /// + /// The query string to redact. + /// The redaction options containing sensitive parameter names and placeholder. + /// The redacted query string, or null if the query string is empty. + public static string? GetRedactedQueryString(QueryString queryString, UrlQueryRedactionOptions options) + { + if (!queryString.HasValue) + { + return null; + } + + var query = queryString.Value; + if (string.IsNullOrEmpty(query)) + { + return null; + } + + var body = query.AsSpan().TrimStart('?'); + + if (body.IsEmpty) { + return query; + } + + var escapedPlaceholder = Uri.EscapeDataString(options.RedactedPlaceholder); + var sb = new StringBuilder(query.Length); + sb.Append('?'); + + var isFirstSegment = true; + + foreach (var segment in body.Split('&')) + { + var pair = body[segment]; + + if (!isFirstSegment) { sb.Append('&'); } + isFirstSegment = false; + + var rawKey = GetKey(pair); + + string decodedKey; + try + { + decodedKey = Uri.UnescapeDataString(rawKey.ToString()); + } + catch (UriFormatException) + { + sb.Append(pair); + continue; + } + + if (options.SensitiveQueryParameters.Contains(decodedKey)) + { + sb.Append(rawKey); + sb.Append('='); + sb.Append(escapedPlaceholder); + } + else + { + sb.Append(pair); + } + } + + return sb.ToString(); + } + + private static ReadOnlySpan GetKey(ReadOnlySpan pair) + { + var eqIndex = pair.IndexOf('='); + return eqIndex == -1 ? pair : pair.Slice(0, eqIndex); + } } diff --git a/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs b/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs new file mode 100644 index 000000000000..7449f8897216 --- /dev/null +++ b/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Hosting; + +/// +/// Options for configuring query string redaction in HTTP telemetry. +/// +public sealed class UrlQueryRedactionOptions +{ + /// + /// Initializes a new instance of . + /// + public UrlQueryRedactionOptions() + { + } + + /// + /// Gets the set of query parameter names whose values should be redacted. + /// Parameter name matching is case-insensitive. + /// + /// + /// Default sensitive parameters include: password, token, api_key, apikey, secret, + /// access_token, refresh_token, credential, key, sig, signature. + /// + public HashSet SensitiveQueryParameters { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "password", + "pwd", + "token", + "api_key", + "apikey", + "secret", + "access_token", + "refresh_token", + "credential", + "key", + "sig", + "signature" + }; + + /// + /// Gets or sets the placeholder text used to replace redacted values. + /// + /// Defaults to "[Redacted]". + public string RedactedPlaceholder { get; set; } = "[Redacted]"; +} diff --git a/src/Hosting/Hosting/src/Internal/WebHost.cs b/src/Hosting/Hosting/src/Internal/WebHost.cs index e0b0857d982f..d7a3e5b2048e 100644 --- a/src/Hosting/Hosting/src/Internal/WebHost.cs +++ b/src/Hosting/Hosting/src/Internal/WebHost.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Hosting; @@ -143,7 +144,8 @@ public async Task StartAsync(CancellationToken cancellationToken = default) var propagator = _applicationServices.GetRequiredService(); var httpContextFactory = _applicationServices.GetRequiredService(); var hostingMetrics = _applicationServices.GetRequiredService(); - var hostingApp = new HostingApplication(application, _logger, diagnosticSource, activitySource, propagator, httpContextFactory, HostingEventSource.Log, hostingMetrics); + var urlQueryRedactionOptions = _applicationServices.GetService>()?.Value; + var hostingApp = new HostingApplication(application, _logger, diagnosticSource, activitySource, propagator, httpContextFactory, HostingEventSource.Log, hostingMetrics, urlQueryRedactionOptions); await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false); _startedServer = true; diff --git a/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt b/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..2cccc69348d0 100644 --- a/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt +++ b/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt @@ -1 +1,6 @@ #nullable enable +Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions +Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.UrlQueryRedactionOptions() -> void +Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.SensitiveQueryParameters.get -> System.Collections.Generic.HashSet! +Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.RedactedPlaceholder.get -> string! +Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.RedactedPlaceholder.set -> void diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index 73ad24d8358a..0e5b10536d9a 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -1182,6 +1182,170 @@ static void AssertKeyValuePair(KeyValuePair pair, string key, T va } } + [Fact] + public void ActivityListeners_QueryStringRedacted_WhenOptionsConfigured() + { + var testSource = new ActivitySource(Path.GetRandomFileName()); + var redactionOptions = new UrlQueryRedactionOptions(); + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false, urlQueryRedactionOptions: redactionOptions); + var tags = new Dictionary(); + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => + { + tags = Activity.Current.TagObjects.ToDictionary(); + } + }; + + ActivitySource.AddActivityListener(listener); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"host", "localhost:8080" } + }, + Path = "/search", + QueryString = "?q=laptop&token=secret123&page=2", + Scheme = "http", + Method = "GET", + }); + + hostingApplication.CreateContext(features); + + Assert.True(tags.TryGetValue("url.query", out var queryValue)); + var query = Assert.IsType(queryValue); + Assert.Contains("q=laptop", query); + Assert.Contains("page=2", query); + Assert.Contains("token=%5BRedacted%5D", query); // [Redacted] URL encoded + Assert.DoesNotContain("secret123", query); + } + + [Fact] + public void ActivityListeners_QueryStringNotIncluded_WhenOptionsNotConfigured() + { + var testSource = new ActivitySource(Path.GetRandomFileName()); + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false, urlQueryRedactionOptions: null); + var tags = new Dictionary(); + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => + { + tags = Activity.Current.TagObjects.ToDictionary(); + } + }; + + ActivitySource.AddActivityListener(listener); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"host", "localhost:8080" } + }, + Path = "/search", + QueryString = "?q=laptop&token=secret123", + Scheme = "http", + Method = "GET", + }); + + hostingApplication.CreateContext(features); + + Assert.False(tags.ContainsKey("url.query")); + } + + [Fact] + public void ActivityListeners_QueryStringWithCustomPlaceholder() + { + var testSource = new ActivitySource(Path.GetRandomFileName()); + var redactionOptions = new UrlQueryRedactionOptions + { + RedactedPlaceholder = "***" + }; + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false, urlQueryRedactionOptions: redactionOptions); + var tags = new Dictionary(); + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => + { + tags = Activity.Current.TagObjects.ToDictionary(); + } + }; + + ActivitySource.AddActivityListener(listener); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"host", "localhost" } + }, + Path = "/api", + QueryString = "?password=mysecret", + Scheme = "https", + Method = "POST", + }); + + hostingApplication.CreateContext(features); + + Assert.True(tags.TryGetValue("url.query", out var queryValue)); + var query = Assert.IsType(queryValue); + Assert.Contains("password=%2A%2A%2A", query); // *** URL encoded + Assert.DoesNotContain("mysecret", query); + } + + [Fact] + public void ActivityListeners_QueryStringWithCustomSensitiveParameters() + { + var testSource = new ActivitySource(Path.GetRandomFileName()); + var redactionOptions = new UrlQueryRedactionOptions(); + redactionOptions.SensitiveQueryParameters.Clear(); + redactionOptions.SensitiveQueryParameters.Add("credit_card"); + redactionOptions.SensitiveQueryParameters.Add("ssn"); + + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false, urlQueryRedactionOptions: redactionOptions); + var tags = new Dictionary(); + using var listener = new ActivityListener + { + ShouldListenTo = activitySource => ReferenceEquals(activitySource, testSource), + Sample = (ref ActivityCreationOptions _) => ActivitySamplingResult.AllData, + ActivityStarted = activity => + { + tags = Activity.Current.TagObjects.ToDictionary(); + } + }; + + ActivitySource.AddActivityListener(listener); + + features.Set(new HttpRequestFeature() + { + Headers = new HeaderDictionary() + { + {"host", "localhost" } + }, + Path = "/checkout", + QueryString = "?credit_card=1234567890&ssn=123-45-6789&amount=100", + Scheme = "https", + Method = "POST", + }); + + hostingApplication.CreateContext(features); + + Assert.True(tags.TryGetValue("url.query", out var queryValue)); + var query = Assert.IsType(queryValue); + Assert.Contains("amount=100", query); + Assert.Contains("credit_card=%5BRedacted%5D", query); + Assert.Contains("ssn=%5BRedacted%5D", query); + Assert.DoesNotContain("1234567890", query); + Assert.DoesNotContain("123-45-6789", query); + } + [Theory] [InlineData("http", 80)] [InlineData("HTTP", 80)] @@ -1639,7 +1803,7 @@ private static void AssertProperty(object o, string name) private static HostingApplication CreateApplication(out FeatureCollection features, DiagnosticListener diagnosticListener = null, ActivitySource activitySource = null, ILogger logger = null, Action configure = null, HostingEventSource eventSource = null, IMeterFactory meterFactory = null, - bool? suppressActivityOpenTelemetryData = null) + bool? suppressActivityOpenTelemetryData = null, UrlQueryRedactionOptions urlQueryRedactionOptions = null) { var httpContextFactory = new Mock(); @@ -1659,7 +1823,8 @@ private static HostingApplication CreateApplication(out FeatureCollection featur DistributedContextPropagator.CreateDefaultPropagator(), httpContextFactory.Object, eventSource ?? HostingEventSource.Log, - new HostingMetrics(meterFactory ?? new TestMeterFactory())); + new HostingMetrics(meterFactory ?? new TestMeterFactory()), + urlQueryRedactionOptions); if (suppressActivityOpenTelemetryData is { } suppress) { From c03615fe5087d818c019bd97e92e128cd1c4364f Mon Sep 17 00:00:00 2001 From: Claudio Godoy <40471021+claudiogodoy99@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:56:14 -0300 Subject: [PATCH 2/4] Update src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs b/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs index 7449f8897216..1aa6e3011a94 100644 --- a/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs +++ b/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs @@ -20,7 +20,7 @@ public UrlQueryRedactionOptions() /// Parameter name matching is case-insensitive. /// /// - /// Default sensitive parameters include: password, token, api_key, apikey, secret, + /// Default sensitive parameters include: password, pwd, token, api_key, apikey, secret, /// access_token, refresh_token, credential, key, sig, signature. /// public HashSet SensitiveQueryParameters { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) From e2836e91ba9c787d3fe7d491823a93f25b9bd728 Mon Sep 17 00:00:00 2001 From: claudiogodoy99 Date: Sun, 22 Feb 2026 23:51:41 -0300 Subject: [PATCH 3/4] feat(hosting): add IsEnabled flag for opt-in URL query redaction Add explicit opt-in behavior for UrlQueryRedactionOptions to address PR feedback. IOptions is always resolvable when AddOptions() is called, making nullable parameters ineffective for opt-in features. Changes: - Add IsEnabled property (defaults to false) to UrlQueryRedactionOptions - Check IsEnabled in HostingApplicationDiagnostics before redacting - Update PublicAPI.Unshipped.txt with new API surface - Fix formatting/indentation in HostingTelemetryHelpers.cs - Update tests to explicitly enable redaction when testing Users must now explicitly opt-in: services.Configure(o => o.IsEnabled = true); --- .../Hosting/src/GenericHost/GenericWebHostService.cs | 6 +++--- .../Hosting/src/Internal/HostingApplication.cs | 2 +- .../src/Internal/HostingApplicationDiagnostics.cs | 8 ++++---- .../Hosting/src/Internal/HostingTelemetryHelpers.cs | 10 +++++++--- .../Hosting/src/Internal/UrlQueryRedactionOptions.cs | 6 ++++++ src/Hosting/Hosting/src/Internal/WebHost.cs | 2 +- src/Hosting/Hosting/src/PublicAPI.Unshipped.txt | 2 ++ .../test/HostingApplicationDiagnosticsTests.cs | 12 ++++++++---- 8 files changed, 32 insertions(+), 16 deletions(-) diff --git a/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs b/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs index afc8ffcbb160..c838d61aed57 100644 --- a/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs +++ b/src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs @@ -28,7 +28,7 @@ public GenericWebHostService(IOptions options, IConfiguration configuration, IWebHostEnvironment hostingEnvironment, HostingMetrics hostingMetrics, - IOptions? urlQueryRedactionOptions) + IOptions urlQueryRedactionOptions) { Options = options.Value; Server = server; @@ -43,7 +43,7 @@ public GenericWebHostService(IOptions options, Configuration = configuration; HostingEnvironment = hostingEnvironment; HostingMetrics = hostingMetrics; - UrlQueryRedactionOptions = urlQueryRedactionOptions?.Value; + UrlQueryRedactionOptions = urlQueryRedactionOptions.Value; } public GenericWebHostServiceOptions Options { get; } @@ -60,7 +60,7 @@ public GenericWebHostService(IOptions options, public IConfiguration Configuration { get; } public IWebHostEnvironment HostingEnvironment { get; } public HostingMetrics HostingMetrics { get; } - public UrlQueryRedactionOptions? UrlQueryRedactionOptions { get; } + public UrlQueryRedactionOptions UrlQueryRedactionOptions { get; } public async Task StartAsync(CancellationToken cancellationToken) { diff --git a/src/Hosting/Hosting/src/Internal/HostingApplication.cs b/src/Hosting/Hosting/src/Internal/HostingApplication.cs index 3946c8290a0a..d3657e0fd93b 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplication.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplication.cs @@ -33,7 +33,7 @@ public HostingApplication( IHttpContextFactory httpContextFactory, HostingEventSource eventSource, HostingMetrics metrics, - UrlQueryRedactionOptions? urlQueryRedactionOptions = null) + UrlQueryRedactionOptions urlQueryRedactionOptions) { _application = application; _diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource, activitySource, propagator, eventSource, metrics, urlQueryRedactionOptions); diff --git a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs index ce9d0065e23e..7e9d7837127d 100644 --- a/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs +++ b/src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs @@ -34,7 +34,7 @@ internal sealed class HostingApplicationDiagnostics private readonly HostingEventSource _eventSource; private readonly HostingMetrics _metrics; private readonly ILogger _logger; - private readonly UrlQueryRedactionOptions? _urlQueryRedactionOptions; + private readonly UrlQueryRedactionOptions _urlQueryRedactionOptions; // Internal for testing purposes only internal bool SuppressActivityOpenTelemetryData { get; set; } @@ -46,7 +46,7 @@ public HostingApplicationDiagnostics( DistributedContextPropagator propagator, HostingEventSource eventSource, HostingMetrics metrics, - UrlQueryRedactionOptions? urlQueryRedactionOptions) + UrlQueryRedactionOptions urlQueryRedactionOptions) { _logger = logger; _diagnosticListener = diagnosticListener; @@ -460,7 +460,7 @@ private void RecordRequestStartMetrics(HttpContext httpContext) return activity; } - private static TagList CreateInitializeActivityTags(HttpContext httpContext, UrlQueryRedactionOptions? urlQueryRedactionOptions) + private static TagList CreateInitializeActivityTags(HttpContext httpContext, UrlQueryRedactionOptions urlQueryRedactionOptions) { // The tags here are set when the activity is created. They can be used in sampling decisions. // Most values in semantic conventions that are present at creation are specified: @@ -499,7 +499,7 @@ private static TagList CreateInitializeActivityTags(HttpContext httpContext, Url var path = (request.PathBase.HasValue || request.Path.HasValue) ? (request.PathBase + request.Path).ToString() : "/"; creationTags.Add(HostingTelemetryHelpers.AttributeUrlPath, path); - if (urlQueryRedactionOptions != null && request.QueryString.HasValue) + if (urlQueryRedactionOptions.IsEnabled && request.QueryString.HasValue) { var redactedQuery = HostingTelemetryHelpers.GetRedactedQueryString(request.QueryString, urlQueryRedactionOptions); if (redactedQuery != null) diff --git a/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs b/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs index 1e60f4a1653e..b10a934880a4 100644 --- a/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs +++ b/src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs @@ -155,7 +155,7 @@ public static string GetActivityDisplayName(string originalHttpMethod, string? h /// The query string to redact. /// The redaction options containing sensitive parameter names and placeholder. /// The redacted query string, or null if the query string is empty. - public static string? GetRedactedQueryString(QueryString queryString, UrlQueryRedactionOptions options) + public static string? GetRedactedQueryString(QueryString queryString, UrlQueryRedactionOptions options) { if (!queryString.HasValue) { @@ -170,7 +170,8 @@ public static string GetActivityDisplayName(string originalHttpMethod, string? h var body = query.AsSpan().TrimStart('?'); - if (body.IsEmpty) { + if (body.IsEmpty) + { return query; } @@ -184,7 +185,10 @@ public static string GetActivityDisplayName(string originalHttpMethod, string? h { var pair = body[segment]; - if (!isFirstSegment) { sb.Append('&'); } + if (!isFirstSegment) + { + sb.Append('&'); + } isFirstSegment = false; var rawKey = GetKey(pair); diff --git a/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs b/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs index 1aa6e3011a94..467cbdd2d4db 100644 --- a/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs +++ b/src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs @@ -15,6 +15,12 @@ public UrlQueryRedactionOptions() { } + /// + /// Gets or sets a value indicating whether URL query string redaction is enabled. + /// + /// Defaults to false. Set to true to enable query string redaction. + public bool IsEnabled { get; set; } + /// /// Gets the set of query parameter names whose values should be redacted. /// Parameter name matching is case-insensitive. diff --git a/src/Hosting/Hosting/src/Internal/WebHost.cs b/src/Hosting/Hosting/src/Internal/WebHost.cs index d7a3e5b2048e..018f2c6e782c 100644 --- a/src/Hosting/Hosting/src/Internal/WebHost.cs +++ b/src/Hosting/Hosting/src/Internal/WebHost.cs @@ -144,7 +144,7 @@ public async Task StartAsync(CancellationToken cancellationToken = default) var propagator = _applicationServices.GetRequiredService(); var httpContextFactory = _applicationServices.GetRequiredService(); var hostingMetrics = _applicationServices.GetRequiredService(); - var urlQueryRedactionOptions = _applicationServices.GetService>()?.Value; + var urlQueryRedactionOptions = _applicationServices.GetRequiredService>().Value; var hostingApp = new HostingApplication(application, _logger, diagnosticSource, activitySource, propagator, httpContextFactory, HostingEventSource.Log, hostingMetrics, urlQueryRedactionOptions); await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false); _startedServer = true; diff --git a/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt b/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt index 2cccc69348d0..bb0f13bf79da 100644 --- a/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt +++ b/src/Hosting/Hosting/src/PublicAPI.Unshipped.txt @@ -1,6 +1,8 @@ #nullable enable Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.UrlQueryRedactionOptions() -> void +Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.IsEnabled.get -> bool +Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.IsEnabled.set -> void Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.SensitiveQueryParameters.get -> System.Collections.Generic.HashSet! Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.RedactedPlaceholder.get -> string! Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.RedactedPlaceholder.set -> void diff --git a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs index 0e5b10536d9a..f5af7c3f3944 100644 --- a/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs @@ -1186,7 +1186,7 @@ static void AssertKeyValuePair(KeyValuePair pair, string key, T va public void ActivityListeners_QueryStringRedacted_WhenOptionsConfigured() { var testSource = new ActivitySource(Path.GetRandomFileName()); - var redactionOptions = new UrlQueryRedactionOptions(); + var redactionOptions = new UrlQueryRedactionOptions { IsEnabled = true }; var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false, urlQueryRedactionOptions: redactionOptions); var tags = new Dictionary(); using var listener = new ActivityListener @@ -1224,10 +1224,11 @@ public void ActivityListeners_QueryStringRedacted_WhenOptionsConfigured() } [Fact] - public void ActivityListeners_QueryStringNotIncluded_WhenOptionsNotConfigured() + public void ActivityListeners_QueryStringNotIncluded_WhenOptionsNotEnabled() { var testSource = new ActivitySource(Path.GetRandomFileName()); - var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false, urlQueryRedactionOptions: null); + // IsEnabled defaults to false + var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false); var tags = new Dictionary(); using var listener = new ActivityListener { @@ -1264,6 +1265,7 @@ public void ActivityListeners_QueryStringWithCustomPlaceholder() var testSource = new ActivitySource(Path.GetRandomFileName()); var redactionOptions = new UrlQueryRedactionOptions { + IsEnabled = true, RedactedPlaceholder = "***" }; var hostingApplication = CreateApplication(out var features, activitySource: testSource, suppressActivityOpenTelemetryData: false, urlQueryRedactionOptions: redactionOptions); @@ -1304,7 +1306,7 @@ public void ActivityListeners_QueryStringWithCustomPlaceholder() public void ActivityListeners_QueryStringWithCustomSensitiveParameters() { var testSource = new ActivitySource(Path.GetRandomFileName()); - var redactionOptions = new UrlQueryRedactionOptions(); + var redactionOptions = new UrlQueryRedactionOptions { IsEnabled = true }; redactionOptions.SensitiveQueryParameters.Clear(); redactionOptions.SensitiveQueryParameters.Add("credit_card"); redactionOptions.SensitiveQueryParameters.Add("ssn"); @@ -1805,6 +1807,8 @@ private static HostingApplication CreateApplication(out FeatureCollection featur Action configure = null, HostingEventSource eventSource = null, IMeterFactory meterFactory = null, bool? suppressActivityOpenTelemetryData = null, UrlQueryRedactionOptions urlQueryRedactionOptions = null) { + urlQueryRedactionOptions ??= new UrlQueryRedactionOptions(); + var httpContextFactory = new Mock(); features = new FeatureCollection(); From fc7fc798658369d7a2267125bc4466202af14836 Mon Sep 17 00:00:00 2001 From: claudiogodoy99 Date: Mon, 23 Feb 2026 15:13:11 -0300 Subject: [PATCH 4/4] test(hosting): fix missing parameter on test --- src/Hosting/Hosting/test/HostingApplicationTests.cs | 3 ++- src/Hosting/Hosting/test/HostingMetricsTests.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Hosting/Hosting/test/HostingApplicationTests.cs b/src/Hosting/Hosting/test/HostingApplicationTests.cs index aea50f7eb2f5..778d17f352cf 100644 --- a/src/Hosting/Hosting/test/HostingApplicationTests.cs +++ b/src/Hosting/Hosting/test/HostingApplicationTests.cs @@ -199,7 +199,8 @@ private static HostingApplication CreateApplication(IHttpContextFactory httpCont DistributedContextPropagator.CreateDefaultPropagator(), httpContextFactory, HostingEventSource.Log, - new HostingMetrics(meterFactory ?? new TestMeterFactory())); + new HostingMetrics(meterFactory ?? new TestMeterFactory()), + new UrlQueryRedactionOptions()); return hostingApplication; } diff --git a/src/Hosting/Hosting/test/HostingMetricsTests.cs b/src/Hosting/Hosting/test/HostingMetricsTests.cs index e1f8c0d75def..d4fae8330b09 100644 --- a/src/Hosting/Hosting/test/HostingMetricsTests.cs +++ b/src/Hosting/Hosting/test/HostingMetricsTests.cs @@ -288,7 +288,8 @@ private static HostingApplication CreateApplication(IHttpContextFactory httpCont DistributedContextPropagator.CreateDefaultPropagator(), httpContextFactory, HostingEventSource.Log, - new HostingMetrics(meterFactory ?? new TestMeterFactory())); + new HostingMetrics(meterFactory ?? new TestMeterFactory()), + new UrlQueryRedactionOptions()); return hostingApplication; }