Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
IEnumerable<IStartupFilter> startupFilters,
IConfiguration configuration,
IWebHostEnvironment hostingEnvironment,
HostingMetrics hostingMetrics)
HostingMetrics hostingMetrics,
IOptions<UrlQueryRedactionOptions> urlQueryRedactionOptions)
{
Options = options.Value;
Server = server;
Expand All @@ -42,6 +43,7 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
HostingMetrics = hostingMetrics;
UrlQueryRedactionOptions = urlQueryRedactionOptions.Value;
}

public GenericWebHostServiceOptions Options { get; }
Expand All @@ -58,6 +60,7 @@ public GenericWebHostService(IOptions<GenericWebHostServiceOptions> options,
public IConfiguration Configuration { get; }
public IWebHostEnvironment HostingEnvironment { get; }
public HostingMetrics HostingMetrics { get; }
public UrlQueryRedactionOptions UrlQueryRedactionOptions { get; }

public async Task StartAsync(CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -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();
Expand Down
5 changes: 3 additions & 2 deletions src/Hosting/Hosting/src/Internal/HostingApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ public HostingApplication(
DistributedContextPropagator propagator,
IHttpContextFactory httpContextFactory,
HostingEventSource eventSource,
HostingMetrics metrics)
HostingMetrics metrics,
UrlQueryRedactionOptions urlQueryRedactionOptions)
{
_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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -44,14 +45,16 @@ public HostingApplicationDiagnostics(
ActivitySource activitySource,
DistributedContextPropagator propagator,
HostingEventSource eventSource,
HostingMetrics metrics)
HostingMetrics metrics,
UrlQueryRedactionOptions urlQueryRedactionOptions)
{
_logger = logger;
_diagnosticListener = diagnosticListener;
_activitySource = activitySource;
_propagator = propagator;
_eventSource = eventSource;
_metrics = metrics;
_urlQueryRedactionOptions = urlQueryRedactionOptions;

SuppressActivityOpenTelemetryData = GetSuppressActivityOpenTelemetryData();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.<key>
//
// Note that these tags are added even if Activity.IsAllDataRequested is false, as they may be used in sampling decisions.
Expand Down Expand Up @@ -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.IsEnabled && request.QueryString.HasValue)
{
var redactedQuery = HostingTelemetryHelpers.GetRedactedQueryString(request.QueryString, urlQueryRedactionOptions);
if (redactedQuery != null)
{
creationTags.Add(HostingTelemetryHelpers.AttributeUrlQuery, redactedQuery);
}
}

return creationTags;
}

Expand Down
78 changes: 78 additions & 0 deletions src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -146,4 +148,80 @@ public static string GetActivityDisplayName(string originalHttpMethod, string? h

return string.IsNullOrEmpty(httpRoute) ? namePrefix : $"{namePrefix} {httpRoute}";
}

/// <summary>
/// Redacts sensitive query parameter values from a query string.
/// </summary>
/// <param name="queryString">The query string to redact.</param>
/// <param name="options">The redaction options containing sensitive parameter names and placeholder.</param>
/// <returns>The redacted query string, or null if the query string is empty.</returns>
public static string? GetRedactedQueryString(QueryString queryString, UrlQueryRedactionOptions options)
{
if (!queryString.HasValue)
Comment thread
claudiogodoy99 marked this conversation as resolved.
{
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<char> GetKey(ReadOnlySpan<char> pair)
{
var eqIndex = pair.IndexOf('=');
return eqIndex == -1 ? pair : pair.Slice(0, eqIndex);
}
}
53 changes: 53 additions & 0 deletions src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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;

/// <summary>
/// Options for configuring query string redaction in HTTP telemetry.
/// </summary>
public sealed class UrlQueryRedactionOptions
{
/// <summary>
/// Initializes a new instance of <see cref="UrlQueryRedactionOptions"/>.
/// </summary>
public UrlQueryRedactionOptions()
{
}

/// <summary>
/// Gets or sets a value indicating whether URL query string redaction is enabled.
/// </summary>
/// <value>Defaults to <c>false</c>. Set to <c>true</c> to enable query string redaction.</value>
public bool IsEnabled { get; set; }

/// <summary>
/// Gets the set of query parameter names whose values should be redacted.
/// Parameter name matching is case-insensitive.
/// </summary>
/// <remarks>
/// Default sensitive parameters include: password, pwd, token, api_key, apikey, secret,
/// access_token, refresh_token, credential, key, sig, signature.
/// </remarks>
public HashSet<string> SensitiveQueryParameters { get; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"password",
"pwd",
"token",
"api_key",
"apikey",
"secret",
"access_token",
"refresh_token",
"credential",
"key",
"sig",
"signature"
};

/// <summary>
/// Gets or sets the placeholder text used to replace redacted values.
/// </summary>
/// <value>Defaults to "[Redacted]".</value>
public string RedactedPlaceholder { get; set; } = "[Redacted]";
}
4 changes: 3 additions & 1 deletion src/Hosting/Hosting/src/Internal/WebHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -143,7 +144,8 @@ public async Task StartAsync(CancellationToken cancellationToken = default)
var propagator = _applicationServices.GetRequiredService<DistributedContextPropagator>();
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
var hostingMetrics = _applicationServices.GetRequiredService<HostingMetrics>();
var hostingApp = new HostingApplication(application, _logger, diagnosticSource, activitySource, propagator, httpContextFactory, HostingEventSource.Log, hostingMetrics);
var urlQueryRedactionOptions = _applicationServices.GetRequiredService<IOptions<UrlQueryRedactionOptions>>().Value;
var hostingApp = new HostingApplication(application, _logger, diagnosticSource, activitySource, propagator, httpContextFactory, HostingEventSource.Log, hostingMetrics, urlQueryRedactionOptions);
await Server.StartAsync(hostingApp, cancellationToken).ConfigureAwait(false);
_startedServer = true;

Expand Down
7 changes: 7 additions & 0 deletions src/Hosting/Hosting/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1 +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<string!>!
Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.RedactedPlaceholder.get -> string!
Microsoft.AspNetCore.Hosting.UrlQueryRedactionOptions.RedactedPlaceholder.set -> void
Loading
Loading