Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -8,6 +8,8 @@

### Bugs Fixed

- Hardened Azure Monitor ingestion and Live Metrics redirect handling to prevent credentials and telemetry from being forwarded to untrusted destinations.

### Other Changes

## 1.5.0 (2026-04-30)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bugs Fixed

- Hardened ingestion and Live Metrics redirect handling to reject untrusted destinations before replaying telemetry or caching the redirect. Redirect targets must now use HTTPS and match an approved Azure Monitor trust boundary, preventing credentials and telemetry from being forwarded to attacker-controlled endpoints.

### Other Changes

- Customer SDK stats are now on by default; opt out with `APPLICATIONINSIGHTS_SDKSTATS_DISABLED=true`. `dropCode`/`retryCode` dimension values now use the spec's SCREAMING_SNAKE_CASE (e.g. `CLIENT_EXCEPTION`).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ internal async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPi

if (_cache.TryRead(out Uri? redirectUri))
{
// Set up for the redirect
request.Uri.Reset(redirectUri);
if (RedirectPolicyHelper.IsTrustedIngestionRedirect(request.Uri.ToUri(), redirectUri))
{
// Set up for the redirect
request.Uri.Reset(redirectUri);
}
}

if (async)
Expand All @@ -57,6 +60,11 @@ internal async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPi
break;
}

if (!RedirectPolicyHelper.IsTrustedIngestionRedirect(request.Uri.ToUri(), redirectUri))
{
break;
}

response.Dispose();

// Set up for the redirect
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals
{
internal static class RedirectPolicyHelper
{
private static readonly string[] s_allowedRedirectDomainSuffixes =
{
".livediagnostics.monitor.azure.com",
".monitor.azure.com",
".services.visualstudio.com",
".applicationinsights.azure.com",
".monitor.azure.us",
".applicationinsights.azure.us",
".monitor.azure.cn",
".applicationinsights.azure.cn",
};

internal static bool IsTrustedIngestionRedirect(Uri currentUri, Uri redirectUri)
{
if (!IsValidHttpsRedirect(redirectUri) || !currentUri.IsAbsoluteUri)
{
return false;
}

string currentHost = GetCanonicalHost(currentUri);
string redirectHost = GetCanonicalHost(redirectUri);
if (string.IsNullOrEmpty(currentHost) || string.IsNullOrEmpty(redirectHost))
{
return false;
}

if (string.Equals(currentHost, redirectHost, StringComparison.Ordinal))
{
return string.Equals(currentUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
&& currentUri.Port == redirectUri.Port;
}

if (!currentUri.IsDefaultPort || !redirectUri.IsDefaultPort)
{
return false;
}

foreach (string suffix in s_allowedRedirectDomainSuffixes)
{
if (currentHost.EndsWith(suffix, StringComparison.Ordinal)
&& redirectHost.EndsWith(suffix, StringComparison.Ordinal))
{
return true;
}
}

return false;
}

internal static bool IsTrustedLiveMetricsRedirect(Uri redirectUri)
{
if (!IsValidHttpsRedirect(redirectUri) || !redirectUri.IsDefaultPort)
{
return false;
}

string redirectHost = GetCanonicalHost(redirectUri);
foreach (string suffix in s_allowedRedirectDomainSuffixes)
{
if (redirectHost.EndsWith(suffix, StringComparison.Ordinal))
{
return true;
}
}

return false;
}

private static bool IsValidHttpsRedirect(Uri redirectUri) =>
redirectUri.IsAbsoluteUri
&& string.Equals(redirectUri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
&& string.IsNullOrEmpty(redirectUri.UserInfo);

private static string GetCanonicalHost(Uri uri) => uri.IdnHost.TrimEnd('.').ToLowerInvariant();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
using System.Threading.Tasks;
using Azure.Core;
using Azure.Core.Pipeline;
using Azure.Monitor.OpenTelemetry.Exporter.Internals;
using Azure.Monitor.OpenTelemetry.LiveMetrics;

namespace Azure.Monitor.OpenTelemetry.LiveMetrics.Internals
{
internal sealed class LiveMetricsRedirectPolicy : HttpPipelinePolicy
{
private string? _redirectHostValue;
private Uri? _redirectUri;

public override void Process(HttpMessage message, ReadOnlyMemory<HttpPipelinePolicy> pipeline)
{
Expand All @@ -38,9 +39,9 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPip
Request request = message.Request;

// If we have a cached redirect, apply it
if (_redirectHostValue is not null)
if (_redirectUri is not null)
{
request.Uri.Host = _redirectHostValue;
ApplyRedirect(request, _redirectUri);
}

// Process the request
Expand All @@ -54,10 +55,12 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPip
}

// Check for redirection and retry
if (IsRedirection(message.Response, out string? redirectionValue))
if (IsRedirection(message.Response, out string? redirectionValue)
&& Uri.TryCreate(redirectionValue, UriKind.Absolute, out Uri? redirectUri)
&& RedirectPolicyHelper.IsTrustedLiveMetricsRedirect(redirectUri!))
{
Debug.WriteLine($"OnPing: Received Redirection: {redirectionValue}");
AzureMonitorLiveMetricsEventSource.Log.LiveMetricsRedirectReceived(redirectionValue);
AzureMonitorLiveMetricsEventSource.Log.LiveMetricsRedirectReceived(redirectionValue!);

message.Response.Dispose();

Expand All @@ -70,11 +73,10 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPip
// FINAL VALUE:
// https://westus.livediagnostics.monitor.azure.com/QuickPulseService.svc/ping?api-version=2024-04-01-preview&ikey=00000000-0000-0000-0000-000000000000

// Extract the host value from the redirection URI
_redirectHostValue = new Uri(redirectionValue).Host;
_redirectUri = redirectUri;

// Apply redirect
request.Uri.Host = _redirectHostValue;
ApplyRedirect(request, redirectUri!);

// Issue the redirected request.
if (async)
Expand All @@ -91,6 +93,13 @@ private async ValueTask ProcessAsync(HttpMessage message, ReadOnlyMemory<HttpPip
return;
}

private static void ApplyRedirect(Request request, Uri redirectUri)
{
request.Uri.Scheme = redirectUri.Scheme;
request.Uri.Host = redirectUri.Host;
request.Uri.Port = redirectUri.Port;
}

private static bool IsRedirection(Response response, [NotNullWhen(true)] out string? redirectValue)
{
// example: https://westus.livediagnostics.monitor.azure.com/QuickPulseService.svc
Expand Down
Loading
Loading