From 5bd7cc45304938225952f6de1172fd8bafa20af6 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Fri, 15 May 2026 15:00:26 +0100 Subject: [PATCH] Adjust rented buffer handling in claim set parsing Tightens how pooled buffers are sliced and returned during claim set parsing and token creation. Ensures pooled arrays are properly scoped and cleared on return across all call sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JsonWebToken.cs | 2 +- .../JsonWebTokenHandler.CreateToken.cs | 14 +- .../DeflateCompressionProvider.cs | 2 +- .../EncodingUtils.cs | 6 +- .../ClaimSetBufferHandlingTests.cs | 130 ++++++++++++++++++ 5 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 test/Microsoft.IdentityModel.JsonWebTokens.Tests/ClaimSetBufferHandlingTests.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs index 887e20b55b..837b15d40e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebToken.cs @@ -683,7 +683,7 @@ internal JsonClaimSet CreateClaimSet(ReadOnlySpan strSpan, int startIndex, const int MaxStackallocThreshold = 256; Span output = outputSize <= MaxStackallocThreshold ? stackalloc byte[outputSize] - : (rented = ArrayPool.Shared.Rent(outputSize)); + : (rented = ArrayPool.Shared.Rent(outputSize)).AsSpan(0, outputSize); try { diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs index d843d4f34f..e79d99b37a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.CreateToken.cs @@ -231,13 +231,13 @@ int sizeOfEncodedHeaderAndPayloadAsciiBytes finally { if (encodedChars is not null) - ArrayPool.Shared.Return(encodedChars); + ArrayPool.Shared.Return(encodedChars, clearArray: true); #if NET6_0_OR_GREATER if (signatureBytes is not null) - ArrayPool.Shared.Return(signatureBytes); + ArrayPool.Shared.Return(signatureBytes, clearArray: true); #endif if (asciiBytes is not null) - ArrayPool.Shared.Return(asciiBytes); + ArrayPool.Shared.Return(asciiBytes, clearArray: true); writer?.Dispose(); } @@ -573,16 +573,16 @@ int sizeOfEncodedHeaderAndPayloadAsciiBytes finally { if (encodedChars is not null) - ArrayPool.Shared.Return(encodedChars); + ArrayPool.Shared.Return(encodedChars, clearArray: true); #if NET6_0_OR_GREATER if (signatureBytes is not null) - ArrayPool.Shared.Return(signatureBytes); + ArrayPool.Shared.Return(signatureBytes, clearArray: true); #endif if (asciiBytes is not null) - ArrayPool.Shared.Return(asciiBytes); + ArrayPool.Shared.Return(asciiBytes, clearArray: true); if (payloadBytes is not null) - ArrayPool.Shared.Return(payloadBytes); + ArrayPool.Shared.Return(payloadBytes, clearArray: true); writer?.Dispose(); } diff --git a/src/Microsoft.IdentityModel.Tokens/DeflateCompressionProvider.cs b/src/Microsoft.IdentityModel.Tokens/DeflateCompressionProvider.cs index 001326a4c9..c3484e9b4f 100644 --- a/src/Microsoft.IdentityModel.Tokens/DeflateCompressionProvider.cs +++ b/src/Microsoft.IdentityModel.Tokens/DeflateCompressionProvider.cs @@ -104,7 +104,7 @@ public byte[] Decompress(byte[] value) finally { if (chars != null) - ArrayPool.Shared.Return(chars); + ArrayPool.Shared.Return(chars, clearArray: true); } } diff --git a/src/Microsoft.IdentityModel.Tokens/EncodingUtils.cs b/src/Microsoft.IdentityModel.Tokens/EncodingUtils.cs index 817302f32a..2bd837624b 100644 --- a/src/Microsoft.IdentityModel.Tokens/EncodingUtils.cs +++ b/src/Microsoft.IdentityModel.Tokens/EncodingUtils.cs @@ -64,7 +64,7 @@ internal static T PerformEncodingDependentOperation( } finally { - ArrayPool.Shared.Return(bytes); + ArrayPool.Shared.Return(bytes, clearArray: true); } } @@ -108,7 +108,7 @@ internal static T PerformEncodingDependentOperation( } finally { - ArrayPool.Shared.Return(bytes); + ArrayPool.Shared.Return(bytes, clearArray: true); } } @@ -171,7 +171,7 @@ internal static T PerformEncodingDependentOperation( } finally { - ArrayPool.Shared.Return(bytes); + ArrayPool.Shared.Return(bytes, clearArray: true); } } } diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ClaimSetBufferHandlingTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ClaimSetBufferHandlingTests.cs new file mode 100644 index 0000000000..32c1ad9db0 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/ClaimSetBufferHandlingTests.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + /// + /// Exercises the buffer handling path used by JsonWebToken claim-set decoding, + /// covering both the stack-allocated and pooled buffer code paths. + /// + public class ClaimSetBufferHandlingTests + { + /// + /// A payload larger than the stackalloc threshold uses the pooled buffer path. + /// All decoded claim values must round-trip exactly. + /// + [Fact] + public void LargePayload_PooledBufferPath_ClaimsRoundTrip() + { + var claims = new Dictionary + { + ["sub"] = "user@example.com", + ["name"] = "A deliberately long claim value used to exercise the pooled buffer path", + ["role"] = "administrator", + ["department"] = "Engineering - additional padding to grow the encoded payload", + ["description"] = new string('X', 200) + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('k', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var descriptor = new SecurityTokenDescriptor + { + SigningCredentials = creds, + Claims = claims, + Issuer = "https://test-issuer.example.com", + Audience = "https://test-audience.example.com", + }; + + var handler = new JsonWebTokenHandler(); + string tokenString = handler.CreateToken(descriptor); + + var jwt = new JsonWebToken(tokenString); + + Assert.Equal("user@example.com", jwt.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("A deliberately long claim value used to exercise the pooled buffer path", + jwt.Claims.First(c => c.Type == "name").Value); + Assert.Equal("administrator", jwt.Claims.First(c => c.Type == "role").Value); + Assert.Equal("Engineering - additional padding to grow the encoded payload", + jwt.Claims.First(c => c.Type == "department").Value); + Assert.Equal(new string('X', 200), + jwt.Claims.First(c => c.Type == "description").Value); + } + + /// + /// A small payload takes the stack-allocated path. + /// Regression check: parsing must still produce the expected claim values. + /// + [Fact] + public void SmallPayload_StackallocPath_ClaimsRoundTrip() + { + var claims = new Dictionary + { + ["sub"] = "alice", + ["role"] = "user" + }; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('k', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var descriptor = new SecurityTokenDescriptor + { + SigningCredentials = creds, + Claims = claims, + Issuer = "https://issuer.example.com", + Audience = "https://audience.example.com", + }; + + var handler = new JsonWebTokenHandler(); + string tokenString = handler.CreateToken(descriptor); + + var jwt = new JsonWebToken(tokenString); + + Assert.Equal("alice", jwt.Claims.First(c => c.Type == "sub").Value); + Assert.Equal("user", jwt.Claims.First(c => c.Type == "role").Value); + Assert.Equal("https://issuer.example.com", jwt.Issuer); + Assert.Equal("https://audience.example.com", + jwt.Claims.First(c => c.Type == "aud").Value); + } + + /// + /// Repeatedly parses tokens that take the pooled buffer path to confirm + /// that consecutive uses of pooled buffers do not affect parsing results. + /// + [Fact] + public void RepeatedParsing_PooledBufferPath_ProducesConsistentResults() + { + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(new string('k', 128))); + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + var handler = new JsonWebTokenHandler(); + + for (int i = 0; i < 20; i++) + { + string uniqueValue = $"iteration-{i}-" + new string((char)('A' + (i % 26)), 200); + + var descriptor = new SecurityTokenDescriptor + { + SigningCredentials = creds, + Claims = new Dictionary + { + ["seq"] = i.ToString(), + ["payload"] = uniqueValue + }, + Issuer = "https://issuer.example.com", + }; + + string tokenString = handler.CreateToken(descriptor); + var jwt = new JsonWebToken(tokenString); + + Assert.Equal(i.ToString(), jwt.Claims.First(c => c.Type == "seq").Value); + Assert.Equal(uniqueValue, jwt.Claims.First(c => c.Type == "payload").Value); + } + } + } +}