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); + } + } + } +}