CVE-2026-41483
Description
OpenTelemetry.Resources.Azure is the .NET resource detector for Azure environments. In versions 1.15.0-beta.1 and earlier, the AzureVmMetaDataRequestor class makes HTTP requests to the Azure VM instance metadata service and reads the response body into memory without any size limit. An attacker who controls the configured endpoint, or who can intercept traffic to it via a man-in-the-middle attack, can return an arbitrarily large response body. This causes unbounded heap allocation in the consuming process, leading to high transient memory pressure, garbage-collection stalls, or an OutOfMemoryException that terminates the process. As a workaround, disable the Azure VM resource detector or use network-level controls such as firewall rules, mTLS, or a service mesh to prevent man-in-the-middle attacks on the Azure VM instance metadata endpoint. This issue is fixed in version 1.15.1-beta.1, which streams responses rather than buffering them entirely in memory and ignores responses larger than 4 MiB.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
OpenTelemetry.Resources.AzureNuGet | < 1.15.1-beta.1 | 1.15.1-beta.1 |
Affected products
1- Range: <= 1.15.0-beta.1
Patches
19d8a364af919[Resources.Azure] Limit response body size read (#4121)
6 files changed · +339 −87
src/OpenTelemetry.Resources.Azure/AzureVmMetaDataRequestor.cs+32 −7 modified@@ -7,23 +7,48 @@ namespace OpenTelemetry.Resources.Azure; internal static class AzureVmMetaDataRequestor { - private const string AzureVmMetadataEndpointURL = "http://169.254.169.254/metadata/instance/compute?api-version=2021-12-13&format=json"; + private static readonly Uri AzureVmMetadataEndpointUri = new("http://169.254.169.254/metadata/instance/compute?api-version=2021-12-13&format=json"); public static Func<AzureVmMetadataResponse?> GetAzureVmMetaDataResponse { get; internal set; } = GetAzureVmMetaDataResponseDefault; public static AzureVmMetadataResponse? GetAzureVmMetaDataResponseDefault() { - using var httpClient = new HttpClient() { Timeout = TimeSpan.FromSeconds(2) }; + var timeout = TimeSpan.FromSeconds(2); - httpClient.DefaultRequestHeaders.Add("Metadata", "True"); - var res = httpClient.GetStringAsync(new Uri(AzureVmMetadataEndpointURL)).ConfigureAwait(false).GetAwaiter().GetResult(); + using var cts = new CancellationTokenSource(timeout); + using var httpClient = new HttpClient() { Timeout = timeout }; - if (res != null) + return GetAzureVmMetaData(httpClient, cts.Token); + } + + public static AzureVmMetadataResponse? GetAzureVmMetaData( + HttpClient client, + CancellationToken cancellationToken) + { + using var httpRequestMessage = new HttpRequestMessage(); + + httpRequestMessage.RequestUri = AzureVmMetadataEndpointUri; + httpRequestMessage.Method = HttpMethod.Get; + httpRequestMessage.Headers.Add("Metadata", "True"); + +#if NET + using var response = client.Send(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken); +#else +#pragma warning disable CA2025 // Do not pass 'IDisposable' instances into unawaited tasks + using var response = client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false).GetAwaiter().GetResult(); +#pragma warning restore CA2025 // Do not pass 'IDisposable' instances into unawaited tasks +#endif + + response.EnsureSuccessStatusCode(); + + var result = HttpClientHelpers.GetResponseBodyAsString(response, cancellationToken); + + if (!string.IsNullOrEmpty(result)) { #if NET - return JsonSerializer.Deserialize(res, SourceGenerationContext.Default.AzureVmMetadataResponse); + return JsonSerializer.Deserialize(result, SourceGenerationContext.Default.AzureVmMetadataResponse); #else - return JsonSerializer.Deserialize<AzureVmMetadataResponse>(res); + return JsonSerializer.Deserialize<AzureVmMetadataResponse>(result); #endif }
src/OpenTelemetry.Resources.Azure/CHANGELOG.md+3 −0 modified@@ -5,6 +5,9 @@ * Updated OpenTelemetry core component version(s) to `1.15.2`. ([#4080](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4080)) +* Limit how much of the response body is consumed from metadata service HTTP responses. + ([#4121](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4121)) + ## 1.15.0-beta.1 Released 2026-Jan-21
src/OpenTelemetry.Resources.Azure/OpenTelemetry.Resources.Azure.csproj+1 −0 modified@@ -29,6 +29,7 @@ <ItemGroup> <Compile Include="$(RepoRoot)\src\Shared\Guard.cs" Link="Includes\Guard.cs" /> + <Compile Include="$(RepoRoot)\src\Shared\HttpClientHelpers.cs" Link="Includes\HttpClientHelpers.cs" /> <Compile Include="$(RepoRoot)\src\Shared\ResourceSemanticConventions.cs" Link="Includes\ResourceSemanticConventions.cs"/> </ItemGroup>
src/Shared/HttpClientHelpers.cs+14 −9 modified@@ -72,19 +72,24 @@ internal static class HttpClientHelpers totalRead += bytesRead; } - // We've read exactly limit bytes. Check if there's more data. - var probe = new byte[1]; + bool extra = false; + + if (totalRead == limit) + { + // We've read exactly limit bytes. Check if there's more data. + var probe = new byte[1]; #if NETFRAMEWORK || NETSTANDARD - var extra = stream.Read(probe, 0, 1); + extra = stream.Read(probe, 0, 1) > 0; #else - var extra = stream.Read(probe); + extra = stream.Read(probe) > 0; #endif - if (extra > 0 && !allowTruncation) - { - // + 1: we read exactly MaxMessageSize bytes and confirmed at least one more byte exists. - throw new InvalidOperationException($"Response body exceeded the size limit of {limit} bytes."); + if (extra && !allowTruncation) + { + // + 1: we read exactly MaxMessageSize bytes and confirmed at least one more byte exists. + throw new InvalidOperationException($"Response body exceeded the size limit of {limit} bytes."); + } } if (!allowTruncation) @@ -96,7 +101,7 @@ internal static class HttpClientHelpers var encoding = GetEncoding(httpResponse.Content.Headers.ContentType?.CharSet); var result = encoding.GetString(buffer, 0, totalRead); - if (extra > 0) + if (extra) { result += "[TRUNCATED]"; }
test/OpenTelemetry.Instrumentation.Hangfire.Tests/HangfireInstrumentationJobFilterAttributeTests.cs+104 −71 modified@@ -10,33 +10,34 @@ namespace OpenTelemetry.Instrumentation.Hangfire.Tests; [Collection("Hangfire")] -public class HangfireInstrumentationJobFilterAttributeTests : IClassFixture<HangfireFixture> +public class HangfireInstrumentationJobFilterAttributeTests { - private readonly HangfireFixture hangfireFixture; - - public HangfireInstrumentationJobFilterAttributeTests(HangfireFixture hangfireFixture) - { - this.hangfireFixture = hangfireFixture; - } + private readonly HangfireFixture hangfireFixture = new(); [Fact] public async Task Should_Create_Activity() { // Arrange var exportedItems = new List<Activity>(); - using var tel = Sdk.CreateTracerProviderBuilder() - .AddHangfireInstrumentation() - .AddInMemoryExporter(exportedItems) - .SetSampler<AlwaysOnSampler>() - .Build(); - // Act - var jobId = BackgroundJob.Enqueue<TestJob>(x => x.Execute()); - await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + string jobId; + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddHangfireInstrumentation() + .AddInMemoryExporter(exportedItems) + .SetSampler<AlwaysOnSampler>() + .Build()) + { + // Act + jobId = BackgroundJob.Enqueue<TestJob>(x => x.Execute()); + await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + + provider.ForceFlush(); + } // Assert - Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); - var activity = exportedItems.Single(i => (i.GetTagItem("job.id") as string) == jobId); + var activity = Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); + Assert.Contains("JOB TestJob.Execute", activity.DisplayName); Assert.Equal(ActivityKind.Internal, activity.Kind); } @@ -46,18 +47,24 @@ public async Task Should_Create_Activity_With_Status_Error_When_Job_Failed() { // Arrange var exportedItems = new List<Activity>(); - using var tel = Sdk.CreateTracerProviderBuilder() - .AddHangfireInstrumentation() - .AddInMemoryExporter(exportedItems) - .Build(); - // Act - var jobId = BackgroundJob.Enqueue<TestJob>(x => x.ThrowException()); - await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + string jobId; + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddHangfireInstrumentation() + .AddInMemoryExporter(exportedItems) + .Build()) + { + // Act + jobId = BackgroundJob.Enqueue<TestJob>(x => x.ThrowException()); + await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + + provider.ForceFlush(); + } // Assert - Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); - var activity = exportedItems.Single(i => (i.GetTagItem("job.id") as string) == jobId); + var activity = Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); + Assert.Contains("JOB TestJob.ThrowException", activity.DisplayName); Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.Equal(ActivityStatusCode.Error, activity.Status); @@ -70,18 +77,24 @@ public async Task Should_Create_Activity_With_Exception_Event_When_Job_Failed_An { // Arrange var exportedItems = new List<Activity>(); - using var tel = Sdk.CreateTracerProviderBuilder() - .AddHangfireInstrumentation(options => options.RecordException = true) - .AddInMemoryExporter(exportedItems) - .Build(); - // Act - var jobId = BackgroundJob.Enqueue<TestJob>(x => x.ThrowException()); - await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + string jobId; + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddHangfireInstrumentation(options => options.RecordException = true) + .AddInMemoryExporter(exportedItems) + .Build()) + { + // Act + jobId = BackgroundJob.Enqueue<TestJob>(x => x.ThrowException()); + await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + + provider.ForceFlush(); + } // Assert - Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); - var activity = exportedItems.Single(i => (i.GetTagItem("job.id") as string) == jobId); + var activity = Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); + Assert.Contains("JOB TestJob.ThrowException", activity.DisplayName); Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.Equal(ActivityStatusCode.Error, activity.Status); @@ -94,18 +107,24 @@ public async Task Should_Create_Activity_Without_Exception_Event_When_Job_Failed { // Arrange var exportedItems = new List<Activity>(); - using var tel = Sdk.CreateTracerProviderBuilder() - .AddHangfireInstrumentation(options => options.RecordException = false) - .AddInMemoryExporter(exportedItems) - .Build(); - // Act - var jobId = BackgroundJob.Enqueue<TestJob>(x => x.ThrowException()); - await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + string jobId; + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddHangfireInstrumentation(options => options.RecordException = false) + .AddInMemoryExporter(exportedItems) + .Build()) + { + // Act + jobId = BackgroundJob.Enqueue<TestJob>(x => x.ThrowException()); + await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + + provider.ForceFlush(); + } // Assert - Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); - var activity = exportedItems.Single(i => (i.GetTagItem("job.id") as string) == jobId); + var activity = Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); + Assert.Contains("JOB TestJob.ThrowException", activity.DisplayName); Assert.Equal(ActivityKind.Internal, activity.Kind); Assert.Equal(ActivityStatusCode.Error, activity.Status); @@ -118,19 +137,25 @@ public async Task Should_Create_Activity_With_Custom_DisplayName() { // Arrange var exportedItems = new List<Activity>(); - using var tel = Sdk.CreateTracerProviderBuilder() - .AddHangfireInstrumentation(options => options.DisplayNameFunc = backgroundJob => $"JOB {backgroundJob.Id}") - .AddInMemoryExporter(exportedItems) - .SetSampler<AlwaysOnSampler>() - .Build(); - // Act - var jobId = BackgroundJob.Enqueue<TestJob>(x => x.Execute()); - await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + string jobId; + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddHangfireInstrumentation(options => options.DisplayNameFunc = backgroundJob => $"JOB {backgroundJob.Id}") + .AddInMemoryExporter(exportedItems) + .SetSampler<AlwaysOnSampler>() + .Build()) + { + // Act + jobId = BackgroundJob.Enqueue<TestJob>(x => x.Execute()); + await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + + provider.ForceFlush(); + } // Assert - Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); - var activity = exportedItems.Single(i => (i.GetTagItem("job.id") as string) == jobId); + var activity = Assert.Single(exportedItems, i => (i.GetTagItem("job.id") as string) == jobId); + Assert.Contains($"JOB {jobId}", activity.DisplayName); Assert.Equal(ActivityKind.Internal, activity.Kind); } @@ -176,18 +201,22 @@ public async Task Should_Respect_Filter_Option(string filter, bool shouldRecord) var processedItems = new List<Activity>(); var activityProcessor = new ProcessorMock<Activity>(onStart: processedItems.Add); - using var tel = Sdk.CreateTracerProviderBuilder() - .AddHangfireInstrumentation(configure) - .AddProcessor(activityProcessor) - .Build(); + string jobId; - // Act - var jobId = BackgroundJob.Enqueue<TestJob>(x => x.Execute()); - await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddHangfireInstrumentation(configure) + .AddProcessor(activityProcessor) + .Build()) + { + // Act + jobId = BackgroundJob.Enqueue<TestJob>(x => x.Execute()); + await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + + provider.ForceFlush(); + } // Assert - Assert.Single(processedItems); - var activity = processedItems.First(); + var activity = Assert.Single(processedItems); Assert.Equal(shouldRecord, activity.IsAllDataRequested); Assert.Equal(shouldRecord, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); @@ -198,17 +227,21 @@ public async Task Should_Not_Inject_Invalid_Context() { // Arrange var exportedItems = new List<Activity>(); - using var tel = Sdk.CreateTracerProviderBuilder() - .AddHangfireInstrumentation() - .AddInMemoryExporter(exportedItems) - .SetSampler<AlwaysOffSampler>() - .Build(); using var listener = new OpenTelemetryEventListener(); - // Act - var jobId = BackgroundJob.Enqueue<TestJob>(x => x.Execute()); - await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddHangfireInstrumentation() + .AddInMemoryExporter(exportedItems) + .SetSampler<AlwaysOffSampler>() + .Build()) + { + // Act + var jobId = BackgroundJob.Enqueue<TestJob>(x => x.Execute()); + await this.hangfireFixture.WaitJobProcessedAsync(jobId, 5); + + provider.ForceFlush(); + } // Assert Assert.All(listener.Messages, args => Assert.NotEqual("FailedToInjectActivityContext", args.EventName));
test/OpenTelemetry.Resources.Azure.Tests/AzureVmMetaDataRequestorTests.cs+185 −0 added@@ -0,0 +1,185 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#if NETFRAMEWORK +using System.Net.Http; +#endif +using Xunit; + +namespace OpenTelemetry.Resources.Azure.Tests; + +public class AzureVmMetaDataRequestorTests +{ + private const int MessageSizeLimit = 4 * 1024 * 1024; + + [Fact] + public void GetAzureVmMetaData_HttpResponseWithoutContent_ReturnsCorrectResult() + { + // Arrange + using var httpResponse = new HttpResponseMessage() { Content = null }; + var cancellationToken = CancellationToken.None; + + using var handler = new StubHttpMessageHandler(httpResponse); + using var httpClient = new HttpClient(handler); + + // Act + var actual = AzureVmMetaDataRequestor.GetAzureVmMetaData(httpClient, cancellationToken); + + // Assert + Assert.Null(actual); + } + + [Fact] + public void GetAzureVmMetaData_ResponseWithinLimits_ReturnsFullContent() + { + // Arrange + var json = + """ + { + "azEnvironment": "AzurePublicCloud", + "customData": "", + "evictionPolicy": "", + "isHostCompatibilityLayerVm": "false", + "licenseType": "", + "location": "eastus", + "name": "myvm-01", + "offer": "0001-com-ubuntu-server-focal", + "osProfile": { + "adminUsername": "azureuser", + "computerName": "myvm-01", + "disablePasswordAuthentication": "true" + }, + "osType": "Linux", + "placementGroupId": "", + "plan": { + "name": "", + "product": "", + "publisher": "" + }, + "platformFaultDomain": "0", + "platformSubFaultDomain": "", + "platformUpdateDomain": "0", + "priority": "", + "provider": "Microsoft.Compute", + "publicKeys": [ + { + "keyData": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7... user@host", + "path": "/home/azureuser/.ssh/authorized_keys" + } + ], + "publisher": "canonical", + "resourceGroupName": "rg-demo", + "resourceId": "/subscriptions/11111111-2222-3333-4444-555555555555/resourceGroups/rg-demo/providers/Microsoft.Compute/virtualMachines/myvm-01", + "securityProfile": { + "secureBootEnabled": "true", + "virtualTpmEnabled": "true" + }, + "sku": "20_04-lts-gen2", + "storageProfile": { + "dataDisks": [], + "imageReference": { + "id": "", + "offer": "0001-com-ubuntu-server-focal", + "publisher": "canonical", + "sku": "20_04-lts-gen2", + "version": "latest" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "diskSizeGB": "30", + "managedDisk": { + "id": "/subscriptions/11111111-2222-3333-4444-555555555555/resourceGroups/rg-demo/providers/Microsoft.Compute/disks/myvm-01_OsDisk_1_abcdef" + }, + "name": "myvm-01_OsDisk_1_abcdef", + "osType": "Linux", + "vhd": "" + } + }, + "subscriptionId": "11111111-2222-3333-4444-555555555555", + "tags": "env:dev;owner:team-a", + "tagsList": [ + { "name": "env", "value": "dev" }, + { "name": "owner", "value": "team-a" } + ], + "userData": "", + "version": "20.04.202401300", + "vmId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "vmScaleSetName": "", + "vmSize": "Standard_D2s_v5", + "zone": "1" + } + """; + + var cancellationToken = CancellationToken.None; + + using var httpResponse = new HttpResponseMessage() + { + Content = new StringContent(json), + }; + + using var handler = new StubHttpMessageHandler(httpResponse); + using var httpClient = new HttpClient(handler); + + // Act + var actual = AzureVmMetaDataRequestor.GetAzureVmMetaData(httpClient, cancellationToken); + + // Assert + Assert.NotNull(actual); + Assert.Equal("Standard_D2s_v5", actual.VmSize); + } + + [Theory] + [InlineData(1)] + [InlineData(1024)] + [InlineData(2048)] + public void GetAzureVmMetaData_ContentExceedsLimit_Throws(int excess) + { + // Arrange + var content = @"{""vmSize"": """ + new string('C', MessageSizeLimit + excess) + @"""}"; + var cancellationToken = CancellationToken.None; + + using var httpResponse = new HttpResponseMessage() + { + Content = new StringContent(content), + }; + + using var handler = new StubHttpMessageHandler(httpResponse); + using var httpClient = new HttpClient(handler); + + // Act and Assert + Assert.Throws<InvalidOperationException>(() => HttpClientHelpers.GetResponseBodyAsString(httpResponse, cancellationToken)); + } + + [Fact] + public void GetAzureVmMetaData_EmptyDocument_ReturnsResponse() + { + // Arrange + var cancellationToken = CancellationToken.None; + + using var httpResponse = new HttpResponseMessage() + { + Content = new StringContent("{}"), + }; + + using var handler = new StubHttpMessageHandler(httpResponse); + using var httpClient = new HttpClient(handler); + + // Act + var actual = AzureVmMetaDataRequestor.GetAzureVmMetaData(httpClient, cancellationToken); + + // Assert + Assert.NotNull(actual); + } + + private sealed class StubHttpMessageHandler(HttpResponseMessage response) : HttpMessageHandler + { +#if NET + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) => response; + + protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(response); +#else + protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(response); +#endif + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/4121nvdIssue TrackingPatchWEB
- github.com/open-telemetry/opentelemetry-dotnet-contrib/security/advisories/GHSA-vc24-j8c5-2vw4nvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-vc24-j8c5-2vw4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-41483ghsaADVISORY
- github.com/open-telemetry/opentelemetry-dotnet-contrib/commit/9d8a364af919f62c088edd641c554cb720198964ghsaWEB
News mentions
0No linked articles in our index yet.