Skip to content

Commit

Permalink
Merged PR 10199: Set MaximumDeflateSize
Browse files Browse the repository at this point in the history
The Decompress method has been adjusted to only process a maximum number of chars.

----
#### AI-Generated Description
The pull request adds support for limiting the size of decompressed JWT tokens to prevent decompression attacks. The main changes are:

- Adding a `MaximumDeflateSize` property to various classes that handle compression and decompression, such as `DeflateCompressionProvider`, `CompressionProviderFactory`, and `JwtTokenDecryptionParameters`.
- Passing the `MaximumDeflateSize` value to the `DecompressToken` method and checking if the decompressed token exceeds the limit before returning it.
- Adding new unit tests and theory data to verify the functionality and handle different scenarios.
  • Loading branch information
Brent Schmaltz committed Oct 12, 2023
1 parent e986e22 commit ceeff41
Show file tree
Hide file tree
Showing 9 changed files with 194 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1037,7 +1037,8 @@ private string DecryptToken(JsonWebToken jwtToken, TokenValidationParameters val
new JwtTokenDecryptionParameters
{
DecompressionFunction = JwtTokenUtilities.DecompressToken,
Keys = keys
Keys = keys,
MaximumDeflateSize = MaximumTokenSizeInBytes
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ internal class JwtTokenDecryptionParameters
/// <summary>
/// Gets or sets the function used to attempt decompression with.
/// </summary>
public Func<byte[], string, string> DecompressionFunction { get; set; }
public Func<byte[], string, int, string> DecompressionFunction { get; set; }

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

/// <summary>
/// Gets and sets the maximum deflate size in chars that will be processed.
/// </summary>
public int MaximumDeflateSize
{
get;
set;
} = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;

/// <summary>
/// Gets or sets the 'value' of the 'zip' claim.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,13 @@ public static string CreateEncodedSignature(string input, SigningCredentials sig
/// </summary>
/// <param name="tokenBytes"></param>
/// <param name="algorithm"></param>
/// <param name="maximumDeflateSize"></param>
/// <exception cref="ArgumentNullException">if <paramref name="tokenBytes"/> is null.</exception>
/// <exception cref="ArgumentNullException">if <paramref name="algorithm"/> is null.</exception>
/// <exception cref="NotSupportedException">if the decompression <paramref name="algorithm"/> is not supported.</exception>
/// <exception cref="SecurityTokenDecompressionFailedException">if decompression using <paramref name="algorithm"/> fails.</exception>
/// <returns>Decompressed JWT token</returns>
internal static string DecompressToken(byte[] tokenBytes, string algorithm)
internal static string DecompressToken(byte[] tokenBytes, string algorithm, int maximumDeflateSize)
{
if (tokenBytes == null)
throw LogHelper.LogArgumentNullException(nameof(tokenBytes));
Expand All @@ -131,7 +132,7 @@ internal static string DecompressToken(byte[] tokenBytes, string algorithm)
if (!CompressionProviderFactory.Default.IsSupportedAlgorithm(algorithm))
throw LogHelper.LogExceptionMessage(new NotSupportedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10682, LogHelper.MarkAsNonPII(algorithm))));

var compressionProvider = CompressionProviderFactory.Default.CreateCompressionProvider(algorithm);
var compressionProvider = CompressionProviderFactory.Default.CreateCompressionProvider(algorithm, maximumDeflateSize);

var decompressedBytes = compressionProvider.Decompress(tokenBytes);

Expand Down Expand Up @@ -248,7 +249,7 @@ internal static string DecryptJwtToken(
if (string.IsNullOrEmpty(zipAlgorithm))
return Encoding.UTF8.GetString(decryptedTokenBytes);

return decryptionParameters.DecompressionFunction(decryptedTokenBytes, zipAlgorithm);
return decryptionParameters.DecompressionFunction(decryptedTokenBytes, zipAlgorithm, decryptionParameters.MaximumDeflateSize);
}
catch (Exception ex)
{
Expand Down
14 changes: 13 additions & 1 deletion src/Microsoft.IdentityModel.Tokens/CompressionProviderFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ private static bool IsSupportedCompressionAlgorithm(string algorithm)
/// <param name="algorithm">the decompression algorithm.</param>
/// <returns>a <see cref="ICompressionProvider"/>.</returns>
public ICompressionProvider CreateCompressionProvider(string algorithm)
{
return CreateCompressionProvider(algorithm, TokenValidationParameters.DefaultMaximumTokenSizeInBytes);
}

/// <summary>
/// Returns a <see cref="ICompressionProvider"/> for a specific algorithm.
/// </summary>
/// <param name="algorithm">the decompression algorithm.</param>
/// <param name="maximumDeflateSize">the maximum deflate size in chars that will be processed.</param>
/// <returns>a <see cref="ICompressionProvider"/>.</returns>
public ICompressionProvider CreateCompressionProvider(string algorithm, int maximumDeflateSize)
{
if (string.IsNullOrEmpty(algorithm))
throw LogHelper.LogArgumentNullException(nameof(algorithm));
Expand All @@ -86,10 +97,11 @@ public ICompressionProvider CreateCompressionProvider(string algorithm)
return CustomCompressionProvider;

if (algorithm.Equals(CompressionAlgorithms.Deflate))
return new DeflateCompressionProvider();
return new DeflateCompressionProvider { MaximumDeflateSize = maximumDeflateSize };

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

}
}

45 changes: 41 additions & 4 deletions src/Microsoft.IdentityModel.Tokens/DeflateCompressionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.IdentityModel.Logging;
using System;
using System.Buffers;
using System.IO;
using System.IO.Compression;
using System.Text;
Expand All @@ -14,6 +15,8 @@ namespace Microsoft.IdentityModel.Tokens
/// </summary>
public class DeflateCompressionProvider : ICompressionProvider
{
private int _maximumTokenSizeInBytes = TokenValidationParameters.DefaultMaximumTokenSizeInBytes;

/// <summary>
/// Initializes a new instance of the <see cref="DeflateCompressionProvider"/> class used to compress and decompress used the <see cref="CompressionAlgorithms.Deflate"/> algorithm.
/// </summary>
Expand Down Expand Up @@ -41,6 +44,19 @@ public DeflateCompressionProvider(CompressionLevel compressionLevel)
/// </summary>
public CompressionLevel CompressionLevel { get; private set; } = CompressionLevel.Optimal;

/// <summary>
/// Gets and sets the maximum deflate size in chars that will be processed.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">'value' less than 1.</exception>
public int MaximumDeflateSize
{
get => _maximumTokenSizeInBytes;
set => _maximumTokenSizeInBytes = (value < 1) ?
throw LogHelper.LogExceptionMessage(
new ArgumentOutOfRangeException(nameof(value),
LogHelper.FormatInvariant(LogMessages.IDX10101, LogHelper.MarkAsNonPII(value)))) : value;
}

/// <summary>
/// Decompress the value using DEFLATE algorithm.
/// </summary>
Expand All @@ -51,16 +67,37 @@ public byte[] Decompress(byte[] value)
if (value == null)
throw LogHelper.LogArgumentNullException(nameof(value));

using (var inputStream = new MemoryStream(value))
char[] chars = null;
try
{
using (var deflateStream = new DeflateStream(inputStream, CompressionMode.Decompress))
chars = ArrayPool<char>.Shared.Rent(MaximumDeflateSize);
using (var inputStream = new MemoryStream(value))
{
using (var reader = new StreamReader(deflateStream, Encoding.UTF8))
using (var deflateStream = new DeflateStream(inputStream, CompressionMode.Decompress))
{
return Encoding.UTF8.GetBytes(reader.ReadToEnd());
using (var reader = new StreamReader(deflateStream, Encoding.UTF8))
{
// if there is one more char to read, then the token is too large.
int bytesRead = reader.Read(chars, 0, MaximumDeflateSize);
if (reader.Peek() != -1)
{
throw LogHelper.LogExceptionMessage(
new SecurityTokenDecompressionFailedException(
LogHelper.FormatInvariant(
LogMessages.IDX10816,
LogHelper.MarkAsNonPII(MaximumDeflateSize))));
}

return Encoding.UTF8.GetBytes(chars, 0, bytesRead);
}
}
}
}
finally
{
if (chars != null)
ArrayPool<char>.Shared.Return(chars);
}
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.IdentityModel.Tokens/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ internal static class LogMessages
public const string IDX10813 = "IDX10813: Unable to create a {0} from the properties found in the JsonWebKey: '{1}', Exception '{2}'.";
public const string IDX10814 = "IDX10814: Unable to create a {0} from the properties found in the JsonWebKey: '{1}'. Missing: '{2}'.";
public const string IDX10815 = "IDX10815: Depth of JSON: '{0}' exceeds max depth of '{1}'.";
public const string IDX10816 = "IDX10816: Decompressing would result in a token with a size greater than allowed. Maximum size allowed: '{0}'.";

// Base64UrlEncoding
public const string IDX10820 = "IDX10820: Invalid character found in Base64UrlEncoding. Character: '{0}', Encoding: '{1}'.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,7 @@ protected string DecryptToken(JwtSecurityToken jwtToken, TokenValidationParamete
EncodedToken = jwtToken.RawData,
HeaderAsciiBytes = Encoding.ASCII.GetBytes(jwtToken.EncodedHeader),
InitializationVectorBytes = Base64UrlEncoder.DecodeBytes(jwtToken.RawInitializationVector),
MaximumDeflateSize = MaximumTokenSizeInBytes,
Keys = keys,
Zip = jwtToken.Header.Zip,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3579,6 +3579,66 @@ public static TheoryData<CreateTokenTheoryData> JWECompressionTheoryData
}
}

[Theory, MemberData(nameof(JweDecompressSizeTheoryData))]
public void JWEDecompressionSizeTest(JWEDecompressionTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.JWEDecompressionTest", theoryData);

try
{
var handler = new JsonWebTokenHandler();
CompressionProviderFactory.Default = theoryData.CompressionProviderFactory;
var validationResult = handler.ValidateTokenAsync(theoryData.JWECompressionString, theoryData.ValidationParameters).Result;
theoryData.ExpectedException.ProcessException(validationResult.Exception, context);
}
catch (Exception ex)
{
theoryData.ExpectedException.ProcessException(ex, context);
}

TestUtilities.AssertFailIfErrors(context);
}

public static TheoryData<JWEDecompressionTheoryData> JweDecompressSizeTheoryData()
{
// The character 'U' compresses better because UUU in base 64, repeated characters compress best.
JsonWebTokenHandler jwth = new JsonWebTokenHandler();
SecurityKey key = new SymmetricSecurityKey(new byte[256 / 8]);
EncryptingCredentials encryptingCredentials = new EncryptingCredentials(key, "dir", "A128CBC-HS256");
TokenValidationParameters validationParameters = new TokenValidationParameters { TokenDecryptionKey = key };

TheoryData<JWEDecompressionTheoryData> theoryData = new TheoryData<JWEDecompressionTheoryData>();

string payload = System.Text.Json.JsonSerializer.Serialize(new { U = new string('U', 100_000_000), UU = new string('U', 40_000_000) });
string token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
theoryData.Add(new JWEDecompressionTheoryData
{
CompressionProviderFactory = new CompressionProviderFactory(),
ValidationParameters = validationParameters,
JWECompressionString = token,
TestId = "DeflateSizeExceeded",
ExpectedException = new ExpectedException(
typeof(SecurityTokenDecompressionFailedException),
"IDX10679:",
typeof(SecurityTokenDecompressionFailedException))
});

payload = System.Text.Json.JsonSerializer.Serialize(new { U = new string('U', 100_000_000), UU = new string('U', 50_000_000) });
token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
theoryData.Add(new JWEDecompressionTheoryData
{
CompressionProviderFactory = new CompressionProviderFactory(),
ValidationParameters = validationParameters,
JWECompressionString = token,
TestId = "TokenSizeExceeded",
ExpectedException = new ExpectedException(
typeof(ArgumentException),
"IDX10209:")
});

return theoryData;
}

[Theory, MemberData(nameof(JWEDecompressionTheoryData))]
public void JWEDecompressionTest(JWEDecompressionTheoryData theoryData)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
Expand Down Expand Up @@ -1116,6 +1117,67 @@ public void InstanceClaimMappingAndFiltering()
Assert.True(identity.HasClaim("internalClaim", "claimValue"));
}

[Theory, MemberData(nameof(JweDecompressSizeTheoryData))]
public async Task JWEDecompressionSizeTest(JWEDecompressionTheoryData theoryData)
{
var context = TestUtilities.WriteHeader($"{this}.JWEDecompressionTest", theoryData);

try
{
var handler = new JwtSecurityTokenHandler();
CompressionProviderFactory.Default = theoryData.CompressionProviderFactory;
var validationResult = await handler.ValidateTokenAsync(theoryData.JWECompressionString, theoryData.ValidationParameters).ConfigureAwait(false);
theoryData.ExpectedException.ProcessException(validationResult.Exception, context);
}
catch (Exception ex)
{
theoryData.ExpectedException.ProcessException(ex, context);
}

TestUtilities.AssertFailIfErrors(context);
}

public static TheoryData<JWEDecompressionTheoryData> JweDecompressSizeTheoryData()
{
// The character 'U' compresses better because UUU in base 64 is VVVV and repeated characters compress best

JsonWebTokenHandler jwth = new JsonWebTokenHandler();
SecurityKey key = new SymmetricSecurityKey(new byte[256 / 8]);
EncryptingCredentials encryptingCredentials = new EncryptingCredentials(key, "dir", "A128CBC-HS256");
TokenValidationParameters validationParameters = new TokenValidationParameters { TokenDecryptionKey = key };

TheoryData<JWEDecompressionTheoryData> theoryData = new TheoryData<JWEDecompressionTheoryData>();

string payload = System.Text.Json.JsonSerializer.Serialize(new { U = new string('U', 100_000_000), UU = new string('U', 40_000_000) });
string token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
theoryData.Add(new JWEDecompressionTheoryData
{
CompressionProviderFactory = new CompressionProviderFactory(),
ValidationParameters = validationParameters,
JWECompressionString = token,
TestId = "DeflateSizeExceeded",
ExpectedException = new ExpectedException(
typeof(SecurityTokenDecompressionFailedException),
"IDX10679:",
typeof(SecurityTokenDecompressionFailedException))
});

payload = System.Text.Json.JsonSerializer.Serialize(new { U = new string('U', 100_000_000), UU = new string('U', 50_000_000) });
token = jwth.CreateToken(payload, encryptingCredentials, "DEF");
theoryData.Add(new JWEDecompressionTheoryData
{
CompressionProviderFactory = new CompressionProviderFactory(),
ValidationParameters = validationParameters,
JWECompressionString = token,
TestId = "TokenSizeExceeded",
ExpectedException = new ExpectedException(
typeof(ArgumentException),
"IDX10209:")
});

return theoryData;
}

[Theory, MemberData(nameof(JWEDecompressionTheoryData))]
public void JWEDecompressionTest(JWEDecompressionTheoryData theoryData)
{
Expand Down

0 comments on commit ceeff41

Please sign in to comment.