Skip to content

Commit e06dc84

Browse files
author
Brent Schmaltz
committed
Merged PR 10213: Set MaximumDeflateSize
The Decompress method has been adjusted to only process a maximum number of chars. ---- #### AI-Generated Description This pull request adds support for limiting the size of decompressed tokens in the `DeflateCompressionProvider` and the `JwtTokenUtilities` classes. It also adds unit tests for the new functionality and modifies some existing classes to use the new parameters. The main changes are: - Added a `MaximumDeflateSize` property to the `DeflateCompressionProvider` class and the `JwtTokenDecryptionParameters` class. - Added a `maximumDeflateSize` parameter to the `DecompressToken` method in the `JwtTokenUtilities` class and the `DecompressionFunction` delegate. - Added a `JweDecompressSizeTheoryData` class and a `JWEDecompressionSizeTest` method to test the decompression size limit in both the `JsonWebTokenHandler` and the `JwtSecurityTokenHandler` classes. - Modified the `CreateCompressionProvider` method in the `CompressionProviderFactory` class to accept a `maximumDeflateSize` parameter and pass it to the `DeflateCompressionProvider` constructor. - Modified the `DecryptToken` method in the `JsonWebTokenHandler` class and the `DecryptToken` method in the `JwtSecurityTokenHandler` class to pass the `MaximumTokenSizeInBytes` property to the `DecompressToken` method.
1 parent 0b2f269 commit e06dc84

File tree

9 files changed

+265
-62
lines changed

9 files changed

+265
-62
lines changed

src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,8 @@ private string DecryptToken(JsonWebToken jwtToken, TokenValidationParameters val
911911
new JwtTokenDecryptionParameters
912912
{
913913
DecompressionFunction = JwtTokenUtilities.DecompressToken,
914-
Keys = keys
914+
Keys = keys,
915+
MaximumDeflateSize = MaximumTokenSizeInBytes
915916
});
916917
}
917918

src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenDecryptionParameters.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ internal class JwtTokenDecryptionParameters
3939
/// <summary>
4040
/// Gets or sets the function used to attempt decompression with.
4141
/// </summary>
42-
public Func<byte[], string, string> DecompressionFunction { get; set; }
42+
public Func<byte[], string, int, string> DecompressionFunction { get; set; }
4343

4444
/// <summary>
4545
/// Gets or sets the encryption algorithm (Enc) of the token.
@@ -66,6 +66,15 @@ internal class JwtTokenDecryptionParameters
6666
/// </summary>
6767
public IEnumerable<SecurityKey> Keys { get; set; }
6868

69+
/// <summary>
70+
/// Gets and sets the maximum deflate size in chars that will be processed.
71+
/// </summary>
72+
public int MaximumDeflateSize
73+
{
74+
get;
75+
set;
76+
} = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;
77+
6978
/// <summary>
7079
/// Gets or sets the 'value' of the 'zip' claim.
7180
/// </summary>

src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,15 @@ public static string CreateEncodedSignature(string input, SigningCredentials sig
112112
/// <summary>
113113
/// Decompress JWT token bytes.
114114
/// </summary>
115-
/// <param name="tokenBytes"></param>
116-
/// <param name="algorithm"></param>
115+
/// <param name="tokenBytes">the bytes to be decompressed.</param>
116+
/// <param name="algorithm">the decompress algorithm.</param>
117+
/// <param name="maximumDeflateSize">maximum number of chars that will be decompressed.</param>
117118
/// <exception cref="ArgumentNullException">if <paramref name="tokenBytes"/> is null.</exception>
118119
/// <exception cref="ArgumentNullException">if <paramref name="algorithm"/> is null.</exception>
119120
/// <exception cref="NotSupportedException">if the decompression <paramref name="algorithm"/> is not supported.</exception>
120121
/// <exception cref="SecurityTokenDecompressionFailedException">if decompression using <paramref name="algorithm"/> fails.</exception>
121122
/// <returns>Decompressed JWT token</returns>
122-
internal static string DecompressToken(byte[] tokenBytes, string algorithm)
123+
internal static string DecompressToken(byte[] tokenBytes, string algorithm, int maximumDeflateSize)
123124
{
124125
if (tokenBytes == null)
125126
throw LogHelper.LogArgumentNullException(nameof(tokenBytes));
@@ -130,7 +131,7 @@ internal static string DecompressToken(byte[] tokenBytes, string algorithm)
130131
if (!CompressionProviderFactory.Default.IsSupportedAlgorithm(algorithm))
131132
throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10682, LogHelper.MarkAsNonPII(algorithm))));
132133

133-
var compressionProvider = CompressionProviderFactory.Default.CreateCompressionProvider(algorithm);
134+
var compressionProvider = CompressionProviderFactory.Default.CreateCompressionProvider(algorithm, maximumDeflateSize);
134135

135136
var decompressedBytes = compressionProvider.Decompress(tokenBytes);
136137

@@ -241,7 +242,7 @@ internal static string DecryptJwtToken(
241242
if (string.IsNullOrEmpty(zipAlgorithm))
242243
return Encoding.UTF8.GetString(decryptedTokenBytes);
243244

244-
return decryptionParameters.DecompressionFunction(decryptedTokenBytes, zipAlgorithm);
245+
return decryptionParameters.DecompressionFunction(decryptedTokenBytes, zipAlgorithm, decryptionParameters.MaximumDeflateSize);
245246
}
246247
catch (Exception ex)
247248
{

src/Microsoft.IdentityModel.Tokens/CompressionProviderFactory.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ private static bool IsSupportedCompressionAlgorithm(string algorithm)
7878
/// <param name="algorithm">the decompression algorithm.</param>
7979
/// <returns>a <see cref="ICompressionProvider"/>.</returns>
8080
public ICompressionProvider CreateCompressionProvider(string algorithm)
81+
{
82+
return CreateCompressionProvider(algorithm, TokenValidationParameters.DefaultMaximumTokenSizeInBytes);
83+
}
84+
85+
/// <summary>
86+
/// Returns a <see cref="ICompressionProvider"/> for a specific algorithm.
87+
/// </summary>
88+
/// <param name="algorithm">the decompression algorithm.</param>
89+
/// <param name="maximumDeflateSize">the maximum deflate size in chars that will be processed.</param>
90+
/// <returns>a <see cref="ICompressionProvider"/>.</returns>
91+
public ICompressionProvider CreateCompressionProvider(string algorithm, int maximumDeflateSize)
8192
{
8293
if (string.IsNullOrEmpty(algorithm))
8394
throw LogHelper.LogArgumentNullException(nameof(algorithm));
@@ -86,7 +97,7 @@ public ICompressionProvider CreateCompressionProvider(string algorithm)
8697
return CustomCompressionProvider;
8798

8899
if (algorithm.Equals(CompressionAlgorithms.Deflate))
89-
return new DeflateCompressionProvider();
100+
return new DeflateCompressionProvider { MaximumDeflateSize = maximumDeflateSize };
90101

91102
throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(LogMessages.IDX10652, LogHelper.MarkAsNonPII(algorithm))));
92103
}

src/Microsoft.IdentityModel.Tokens/DeflateCompressionProvider.cs

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
using Microsoft.IdentityModel.Logging;
55
using System;
6+
#if NET461_OR_GREATER
7+
using System.Buffers;
8+
#endif
9+
610
using System.IO;
711
using System.IO.Compression;
812
using System.Text;
@@ -14,6 +18,8 @@ namespace Microsoft.IdentityModel.Tokens
1418
/// </summary>
1519
public class DeflateCompressionProvider : ICompressionProvider
1620
{
21+
private int _maximumTokenSizeInBytes = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;
22+
1723
/// <summary>
1824
/// Initializes a new instance of the <see cref="DeflateCompressionProvider"/> class used to compress and decompress used the <see cref="CompressionAlgorithms.Deflate"/> algorithm.
1925
/// </summary>
@@ -41,6 +47,16 @@ public DeflateCompressionProvider(CompressionLevel compressionLevel)
4147
/// </summary>
4248
public CompressionLevel CompressionLevel { get; private set; } = CompressionLevel.Optimal;
4349

50+
/// <summary>
51+
/// Gets and sets the maximum deflate size in chars that will be processed.
52+
/// </summary>
53+
/// <exception cref="ArgumentOutOfRangeException">'value' less than 1.</exception>
54+
public int MaximumDeflateSize
55+
{
56+
get => _maximumTokenSizeInBytes;
57+
set => _maximumTokenSizeInBytes = (value < 1) ? throw LogHelper.LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value), LogHelper.FormatInvariant(LogMessages.IDX10101, LogHelper.MarkAsNonPII(value)))) : value;
58+
}
59+
4460
/// <summary>
4561
/// Decompress the value using DEFLATE algorithm.
4662
/// </summary>
@@ -51,16 +67,43 @@ public byte[] Decompress(byte[] value)
5167
if (value == null)
5268
throw LogHelper.LogArgumentNullException(nameof(value));
5369

54-
using (var inputStream = new MemoryStream(value))
70+
char[] chars = null;
71+
try
5572
{
56-
using (var deflateStream = new DeflateStream(inputStream, CompressionMode.Decompress))
73+
#if NET461_OR_GREATER
74+
chars = ArrayPool<char>.Shared.Rent(MaximumDeflateSize);
75+
#else
76+
chars = new char[MaximumDeflateSize];
77+
#endif
78+
using (var inputStream = new MemoryStream(value))
5779
{
58-
using (var reader = new StreamReader(deflateStream, Encoding.UTF8))
80+
using (var deflateStream = new DeflateStream(inputStream, CompressionMode.Decompress))
5981
{
60-
return Encoding.UTF8.GetBytes(reader.ReadToEnd());
82+
using (var reader = new StreamReader(deflateStream, Encoding.UTF8))
83+
{
84+
// if there is one more char to read, then the token is too large.
85+
int bytesRead = reader.Read(chars, 0, MaximumDeflateSize);
86+
if (reader.Peek() != -1)
87+
{
88+
throw LogHelper.LogExceptionMessage(
89+
new SecurityTokenDecompressionFailedException(
90+
LogHelper.FormatInvariant(
91+
LogMessages.IDX10816,
92+
LogHelper.MarkAsNonPII(MaximumDeflateSize))));
93+
}
94+
95+
return Encoding.UTF8.GetBytes(chars, 0, bytesRead);
96+
}
6197
}
6298
}
6399
}
100+
finally
101+
{
102+
#if NET461_OR_GREATER
103+
if (chars != null)
104+
ArrayPool<char>.Shared.Return(chars);
105+
#endif
106+
}
64107
}
65108

66109
/// <summary>

src/Microsoft.IdentityModel.Tokens/LogMessages.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,8 @@ internal static class LogMessages
239239
public const string IDX10812 = "IDX10812: Unable to create a {0} from the properties found in the JsonWebKey: '{1}'.";
240240
public const string IDX10813 = "IDX10813: Unable to create a {0} from the properties found in the JsonWebKey: '{1}', Exception '{2}'.";
241241
public const string IDX10814 = "IDX10814: Unable to create a {0} from the properties found in the JsonWebKey: '{1}'. Missing: '{2}'.";
242+
public const string IDX10815 = "IDX10815: Depth of JSON: '{0}' exceeds max depth of '{1}'.";
243+
public const string IDX10816 = "IDX10816: Decompressing would result in a token with a size greater than allowed. Maximum size allowed: '{0}'.";
242244

243245
// Base64UrlEncoding
244246
public const string IDX10820 = "IDX10820: Invalid character found in Base64UrlEncoding. Character: '{0}', Encoding: '{1}'.";

src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,7 @@ protected string DecryptToken(JwtSecurityToken jwtToken, TokenValidationParamete
17601760
EncodedToken = jwtToken.RawData,
17611761
HeaderAsciiBytes = Encoding.ASCII.GetBytes(jwtToken.EncodedHeader),
17621762
InitializationVectorBytes = Base64UrlEncoder.DecodeBytes(jwtToken.RawInitializationVector),
1763+
MaximumDeflateSize = MaximumTokenSizeInBytes,
17631764
Keys = keys,
17641765
Zip = jwtToken.Header.Zip,
17651766
});

test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@
88
using System.IdentityModel.Tokens.Jwt.Tests;
99
using System.IO;
1010
using System.Linq;
11-
using System.Runtime.InteropServices;
1211
using System.Security.Claims;
1312
using System.Security.Cryptography;
14-
using System.Security.Policy;
1513
using System.Text;
1614
using System.Threading.Tasks;
1715
using Microsoft.IdentityModel.Json;
@@ -3400,6 +3398,70 @@ public static TheoryData<CreateTokenTheoryData> JWECompressionTheoryData
34003398
}
34013399
}
34023400

3401+
[Theory, MemberData(nameof(JweDecompressSizeTheoryData))]
3402+
public void JWEDecompressionSizeTest(JWEDecompressionTheoryData theoryData)
3403+
{
3404+
var context = TestUtilities.WriteHeader($"{this}.JWEDecompressionTest", theoryData);
3405+
3406+
try
3407+
{
3408+
var handler = new JsonWebTokenHandler();
3409+
CompressionProviderFactory.Default = theoryData.CompressionProviderFactory;
3410+
var validationResult = handler.ValidateTokenAsync(theoryData.JWECompressionString, theoryData.ValidationParameters).Result;
3411+
theoryData.ExpectedException.ProcessException(validationResult.Exception, context);
3412+
}
3413+
catch (Exception ex)
3414+
{
3415+
theoryData.ExpectedException.ProcessException(ex, context);
3416+
}
3417+
3418+
TestUtilities.AssertFailIfErrors(context);
3419+
}
3420+
3421+
public static TheoryData<JWEDecompressionTheoryData> JweDecompressSizeTheoryData()
3422+
{
3423+
// The character 'U' compresses better because UUU in base 64, repeated characters compress best.
3424+
JsonWebTokenHandler jwth = new JsonWebTokenHandler();
3425+
SecurityKey key = new SymmetricSecurityKey(new byte[256 / 8]);
3426+
EncryptingCredentials encryptingCredentials = new EncryptingCredentials(key, "dir", "A128CBC-HS256");
3427+
TokenValidationParameters validationParameters = new TokenValidationParameters { TokenDecryptionKey = key };
3428+
3429+
TheoryData<JWEDecompressionTheoryData> theoryData = new TheoryData<JWEDecompressionTheoryData>();
3430+
string strU = new string('U', 100_000_000);
3431+
string strUU = new string('U', 40_000_000);
3432+
string payload = $@"{{""U"":""{strU}"", ""UU"":""{strUU}""}}";
3433+
3434+
string token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
3435+
theoryData.Add(new JWEDecompressionTheoryData
3436+
{
3437+
CompressionProviderFactory = new CompressionProviderFactory(),
3438+
ValidationParameters = validationParameters,
3439+
JWECompressionString = token,
3440+
TestId = "DeflateSizeExceeded",
3441+
ExpectedException = new ExpectedException(
3442+
typeof(SecurityTokenDecompressionFailedException),
3443+
"IDX10679:",
3444+
typeof(SecurityTokenDecompressionFailedException))
3445+
});
3446+
3447+
strUU = new string('U', 50_000_000);
3448+
payload = $@"{{""U"":""{strU}"", ""UU"":""{strUU}""}}";
3449+
3450+
token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
3451+
theoryData.Add(new JWEDecompressionTheoryData
3452+
{
3453+
CompressionProviderFactory = new CompressionProviderFactory(),
3454+
ValidationParameters = validationParameters,
3455+
JWECompressionString = token,
3456+
TestId = "TokenSizeExceeded",
3457+
ExpectedException = new ExpectedException(
3458+
typeof(ArgumentException),
3459+
"IDX10209:")
3460+
});
3461+
3462+
return theoryData;
3463+
}
3464+
34033465
[Theory, MemberData(nameof(JWEDecompressionTheoryData))]
34043466
public void JWEDecompressionTest(JWEDecompressionTheoryData theoryData)
34053467
{
@@ -3408,7 +3470,7 @@ public void JWEDecompressionTest(JWEDecompressionTheoryData theoryData)
34083470
try
34093471
{
34103472
var handler = new JsonWebTokenHandler();
3411-
CompressionProviderFactory.Default = theoryData.CompressionProviderFactory;
3473+
//CompressionProviderFactory.Default = theoryData.CompressionProviderFactory;
34123474
var validationResult = handler.ValidateToken(theoryData.JWECompressionString, theoryData.ValidationParameters);
34133475
var validatedToken = validationResult.SecurityToken as JsonWebToken;
34143476
if (validationResult.Exception != null)
@@ -3493,29 +3555,32 @@ public static TheoryData<JWEDecompressionTheoryData> JWEDecompressionTheoryData(
34933555
TestId = "InvalidToken",
34943556
ExpectedException = new ExpectedException(typeof(SecurityTokenDecompressionFailedException), "IDX10679:", typeof(InvalidDataException))
34953557
},
3496-
new JWEDecompressionTheoryData
3497-
{
3498-
ValidationParameters = Default.JWECompressionTokenValidationParameters,
3499-
JWECompressionString = ReferenceTokens.JWECompressionTokenWithDEF,
3500-
CompressionProviderFactory = null,
3501-
TestId = "NullCompressionProviderFactory",
3502-
ExpectedException = ExpectedException.ArgumentNullException("IDX10000:")
3503-
},
3504-
new JWEDecompressionTheoryData
3505-
{
3506-
ValidationParameters = Default.JWECompressionTokenValidationParameters,
3507-
CompressionProviderFactory = compressionProviderFactoryForCustom,
3508-
JWECompressionString = ReferenceTokens.JWECompressionTokenWithCustomAlgorithm,
3509-
TestId = "CustomCompressionProviderSucceeds"
3510-
},
3511-
new JWEDecompressionTheoryData
3512-
{
3513-
ValidationParameters = Default.JWECompressionTokenValidationParameters,
3514-
JWECompressionString = ReferenceTokens.JWECompressionTokenWithDEF,
3515-
CompressionProviderFactory = compressionProviderFactoryForCustom2,
3516-
TestId = "CustomCompressionProviderFails",
3517-
ExpectedException = new ExpectedException(typeof(SecurityTokenDecompressionFailedException), "IDX10679:", typeof(SecurityTokenDecompressionFailedException))
3518-
}
3558+
// Skip these tests as they set a static
3559+
// We need to have a replacement model for custom compression
3560+
// https://identitydivision.visualstudio.com/Engineering/_workitems/edit/2719954
3561+
//new JWEDecompressionTheoryData
3562+
//{
3563+
// ValidationParameters = Default.JWECompressionTokenValidationParameters,
3564+
// JWECompressionString = ReferenceTokens.JWECompressionTokenWithDEF,
3565+
// CompressionProviderFactory = null,
3566+
// TestId = "NullCompressionProviderFactory",
3567+
// ExpectedException = ExpectedException.ArgumentNullException("IDX10000:")
3568+
//},
3569+
//new JWEDecompressionTheoryData
3570+
//{
3571+
// ValidationParameters = Default.JWECompressionTokenValidationParameters,
3572+
// CompressionProviderFactory = compressionProviderFactoryForCustom,
3573+
// JWECompressionString = ReferenceTokens.JWECompressionTokenWithCustomAlgorithm,
3574+
// TestId = "CustomCompressionProviderSucceeds"
3575+
//},
3576+
//new JWEDecompressionTheoryData
3577+
//{
3578+
// ValidationParameters = Default.JWECompressionTokenValidationParameters,
3579+
// JWECompressionString = ReferenceTokens.JWECompressionTokenWithDEF,
3580+
// CompressionProviderFactory = compressionProviderFactoryForCustom2,
3581+
// TestId = "CustomCompressionProviderFails",
3582+
// ExpectedException = new ExpectedException(typeof(SecurityTokenDecompressionFailedException), "IDX10679:", typeof(SecurityTokenDecompressionFailedException))
3583+
//}
35193584
};
35203585
}
35213586

0 commit comments

Comments
 (0)