Skip to content

feat(hosting): add url.query redaction for telemetry sensitive parameter#65369

Open
claudiogodoy99 wants to merge 4 commits intodotnet:mainfrom
claudiogodoy99:feature/telemetry-url-query
Open

feat(hosting): add url.query redaction for telemetry sensitive parameter#65369
claudiogodoy99 wants to merge 4 commits intodotnet:mainfrom
claudiogodoy99:feature/telemetry-url-query

Conversation

@claudiogodoy99
Copy link
Copy Markdown
Contributor

Add configurable query string redaction for OpenTelemetry url.query attribute

  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a new feature or change, please open an issue to discuss the change or find an existing issue.

Summary of the changes (Less than 80 chars)

Summary

This PR introduces configurable redaction of sensitive query string parameters in the url.query OpenTelemetry attribute for HTTP request activities, preventing sensitive data (tokens, passwords, API keys) from being exposed in telemetry.

Changes

New UrlQueryRedactionOptions API:

  • Added public UrlQueryRedactionOptions class with configurable:
    • SensitiveQueryParameters: HashSet of parameter names to redact (default includes: token, api_key, apikey, access_token, password, secret, auth)
    • RedactedPlaceholder: Custom placeholder text (default: [Redacted])

Core Integration:

  • Modified GenericWebHostService to retrieve and pass UrlQueryRedactionOptions from DI container
  • Updated HostingApplication constructor to accept optional UrlQueryRedactionOptions
  • Enhanced HostingApplicationDiagnostics to apply redaction when setting url.query activity tag
  • Added HostingTelemetryHelpers.RedactQueryString() method to perform the actual redaction logic with URL encoding

Behavior:

  • When UrlQueryRedactionOptions is NOT configured: The behavior remains exactly as before — the url.query attribute is not included in telemetry (fully backward compatible, no breaking changes)
  • When UrlQueryRedactionOptions IS configured (via DI), the url.query attribute is included with sensitive values redacted
  • Redacted values are URL-encoded to maintain valid query string format

Testing:

  • Added comprehensive unit tests covering:
    • Default sensitive parameter redaction
    • Custom placeholder configuration
    • Custom sensitive parameter lists
    • Behavior when options not configured (verifies backward compatibility)

Motivation

Address issue: #64850

…ters

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.
Copilot AI review requested due to automatic review settings February 9, 2026 12:44
@github-actions github-actions Bot added the area-hosting Includes Hosting label Feb 9, 2026
@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Feb 9, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an opt-in mechanism for emitting the OpenTelemetry url.query activity tag while redacting configured “sensitive” query-string parameters, to prevent secrets from being captured in HTTP server telemetry.

Changes:

  • Introduces a new public UrlQueryRedactionOptions API to configure sensitive parameter names and a redaction placeholder.
  • Updates Hosting diagnostics/tag initialization to optionally add url.query with redaction applied.
  • Adds a helper (HostingTelemetryHelpers.GetRedactedQueryString) and unit tests validating redaction behavior.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Hosting/Hosting/test/HostingApplicationDiagnosticsTests.cs Adds unit tests validating inclusion/exclusion and redaction behavior for url.query.
src/Hosting/Hosting/src/PublicAPI.Unshipped.txt Declares the new public UrlQueryRedactionOptions API surface.
src/Hosting/Hosting/src/Internal/WebHost.cs Attempts to resolve UrlQueryRedactionOptions from DI and pass to HostingApplication.
src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs Adds the new public options type and default sensitive parameter list.
src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs Adds helper to redact sensitive query parameter values.
src/Hosting/Hosting/src/Internal/HostingApplicationDiagnostics.cs Optionally emits url.query tag during activity creation when options are provided.
src/Hosting/Hosting/src/Internal/HostingApplication.cs Plumbs redaction options into diagnostics.
src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs Attempts to resolve/pass redaction options in generic-host startup path.

Comment thread src/Hosting/Hosting/src/Internal/UrlQueryRedactionOptions.cs Outdated
Comment thread src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs Outdated
Comment thread src/Hosting/Hosting/src/GenericHost/GenericWebHostService.cs Outdated
Comment thread src/Hosting/Hosting/src/Internal/WebHost.cs Outdated
Comment thread src/Hosting/Hosting/src/Internal/HostingTelemetryHelpers.cs
@claudiogodoy99
Copy link
Copy Markdown
Contributor Author

claudiogodoy99 commented Feb 9, 2026

Performance Concerns

I've Benchmark some approaches for the new method HostingTelemetryHelpers.RedactQueryString, and that one was the more promising.

Bench Results

| Method             | Categories     | Mean        | Error      | StdDev     | Gen0   | Allocated |
|------------------- |--------------- |------------:|-----------:|-----------:|-------:|----------:|
| ManyParams         | 12_Params      | 439.2046 ns | 20.5173 ns | 13.5709 ns | 0.1125 |     944 B |
|                    |                |             |            |            |        |           |
| AllSensitiveParams | 3_AllSensitive | 201.2194 ns |  9.4309 ns |  4.9325 ns | 0.0715 |     600 B |
|                    |                |             |            |            |        |           |
| NoSensitiveParams  | 3_NonSensitive | 151.1875 ns |  5.1225 ns |  3.3882 ns | 0.0410 |     344 B |
|                    |                |             |            |            |        |           |
| MixedParams        | 5_Mixed        | 279.1219 ns | 14.9330 ns |  9.8773 ns | 0.0858 |     720 B |
|                    |                |             |            |            |        |           |
| EmptyQueryString   | Empty          |   0.9674 ns |  0.0814 ns |  0.0484 ns |      - |         - |
|                    |                |             |            |            |        |           |
| SingleParam        | SingleParam    |  81.2347 ns |  5.7101 ns |  3.7769 ns | 0.0257 |     216 B |
|                    |                |             |            |            |        |           |
| SpecialCharsParams | SpecialChars   | 218.3186 ns |  6.4507 ns |  3.8387 ns | 0.0639 |     536 B |

Bench Code

using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Columns;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
using Microsoft.AspNetCore.Http;

BenchmarkRunner.Run<RedactedQueryStringBenchmarks>();

public class UrlQueryRedactionOptions
{
    public HashSet<string> SensitiveQueryParameters { get; set; } = new(StringComparer.OrdinalIgnoreCase);
    public string RedactedPlaceholder { get; set; } = "***";
}

[MemoryDiagnoser]
[SimpleJob(warmupCount: 3, iterationCount: 10)]
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
[CategoriesColumn]
public class RedactedQueryStringBenchmarks
{
    private QueryString _emptyQueryString;
    private QueryString _noSensitiveParams;
    private QueryString _allSensitiveParams;
    private QueryString _mixedParams;
    private QueryString _singleParam;
    private QueryString _manyParams;
    private QueryString _specialCharsParams;

    private UrlQueryRedactionOptions _options = null!;

    [GlobalSetup]
    public void Setup()
    {
        _options = new UrlQueryRedactionOptions
        {
            SensitiveQueryParameters = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
            {
                "password", "token", "secret", "apiKey", "credit_card"
            },
            RedactedPlaceholder = "***"
        };

        _emptyQueryString = new QueryString(string.Empty);
        _singleParam = new QueryString("?name=John");
        _noSensitiveParams = new QueryString("?name=John&age=30&city=NYC");
        _allSensitiveParams = new QueryString("?password=abc123&token=xyz789&secret=s3cr3t");
        _mixedParams = new QueryString("?name=John&password=abc123&age=30&token=xyz789&city=NYC");
        _manyParams = new QueryString("?a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&password=secret&token=abc&i=9&j=10");
        _specialCharsParams = new QueryString("?name=John+Doe&password=p%40ss+word&city=New+York&token=abc%26def");
    }

    [Benchmark]
    [BenchmarkCategory("Empty")]
    public string? EmptyQueryString() => GetRedactedQueryString(_emptyQueryString, _options);

    [Benchmark]
    [BenchmarkCategory("SingleParam")]
    public string? SingleParam() => GetRedactedQueryString(_singleParam, _options);

    [Benchmark]
    [BenchmarkCategory("3_NonSensitive")]
    public string? NoSensitiveParams() => GetRedactedQueryString(_noSensitiveParams, _options);

    [Benchmark]
    [BenchmarkCategory("3_AllSensitive")]
    public string? AllSensitiveParams() => GetRedactedQueryString(_allSensitiveParams, _options);

    [Benchmark]
    [BenchmarkCategory("5_Mixed")]
    public string? MixedParams() => GetRedactedQueryString(_mixedParams, _options);

    [Benchmark]
    [BenchmarkCategory("12_Params")]
    public string? ManyParams() => GetRedactedQueryString(_manyParams, _options);

    [Benchmark]
    [BenchmarkCategory("SpecialChars")]
    public string? SpecialCharsParams() => GetRedactedQueryString(_specialCharsParams, _options);

    public static string? GetRedactedQueryString(QueryString queryString, UrlQueryRedactionOptions options)
    {
        if (!queryString.HasValue)
        {
            return null;
        }

        var query = queryString.Value;
        if (string.IsNullOrEmpty(query))
        {
            return null;
        }

        // QueryString.Value always starts with '?' (e.g. "?name=John&age=30"),
        // so we strip it here and re-add it to the output below.
        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)
            {
                // Malformed percent-encoding in key — copy segment verbatim
                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);
    }
}

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Add explicit opt-in behavior for UrlQueryRedactionOptions to address
PR feedback. IOptions<T> 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<UrlQueryRedactionOptions>(o => o.IsEnabled = true);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-hosting Includes Hosting community-contribution Indicates that the PR has been added by a community member pending-ci-rerun When assigned to a PR indicates that the CI checks should be rerun

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants