Umbraco.Cms Vulnerable to Disclosure of Configured Password Requirements
Description
Umbraco, a free and open source .NET content management system, has a vulnerability in versions 10.0.0 through 10.8.10 and 13.0.0 through 13.9.1. Via a request to an anonymously authenticated endpoint it's possible to retrieve information about the configured password requirements. The information available is limited but would perhaps give some additional detail useful for someone attempting to brute force derive a user's password. This information was not exposed in Umbraco 7 or 8, nor in 14 or higher versions. The vulnerability is patched in versions 10.8.11 and 13.9.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
Umbraco.CmsNuGet | >= 10.0.0, < 10.8.11 | 10.8.11 |
Umbraco.CmsNuGet | >= 13.0.0, < 13.9.2 | 13.9.2 |
Affected products
1- Range: >= 10.0.0, < 10.8.111
Patches
2b4144564c836Merge commit from fork
3 files changed · +31 −4
src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs+15 −4 modified@@ -131,12 +131,17 @@ public AuthenticationController( AuthorizationPolicies.BackOfficeAccess)] // Needed to enforce the principle set on the request, if one exists. public IDictionary<string, object> GetPasswordConfig(int userId) { + if (HttpContext.HasActivePasswordResetFlowSession(userId)) + { + return _passwordConfiguration.GetConfiguration(); + } + Attempt<int> currentUserId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId() ?? Attempt<int>.Fail(); - return _passwordConfiguration.GetConfiguration( - currentUserId.Success - ? currentUserId.Result != userId - : true); + + return currentUserId.Success + ? _passwordConfiguration.GetConfiguration(currentUserId.Result != userId) + : new Dictionary<string, object>(); } /// <summary> @@ -417,6 +422,8 @@ public async Task<bool> IsAuthenticated() [Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)] public async Task<ActionResult<UserDetail?>> PostLogin(LoginModel loginModel) { + HttpContext.EndPasswordResetFlowSession(); + // Start a timed scope to ensure failed responses return is a consistent time var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.UserMinimumFailedLoginDurationInMilliseconds); await using var timedScope = new TimedScope(loginDuration, HttpContext.RequestAborted); @@ -490,6 +497,8 @@ public async Task<IActionResult> PostRequestPasswordReset(RequestPasswordResetMo return BadRequest(); } + HttpContext.EndPasswordResetFlowSession(); + BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email); await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); // To randomize response time preventing user enumeration @@ -646,6 +655,8 @@ public async Task<IActionResult> PostSend2FACode([FromBody] string provider) [AllowAnonymous] public async Task<IActionResult> PostSetPassword(SetPasswordModel model) { + HttpContext.EndPasswordResetFlowSession(); + BackOfficeIdentityUser? identityUser = await _userManager.FindByIdAsync(model.UserId.ToString(CultureInfo.InvariantCulture)); if (identityUser is null)
src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs+5 −0 modified@@ -402,6 +402,11 @@ public async Task<IActionResult> ValidatePasswordResetCode([Bind(Prefix = "u")] var result = await _userManager.VerifyUserTokenAsync(user, "Default", "ResetPassword", resetCode); + if (result) + { + HttpContext.StartPasswordResetFlowSession(userId); + } + return result ? // Redirect to login with userId and resetCode
src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs+11 −0 modified@@ -5,9 +5,20 @@ namespace Umbraco.Extensions; public static class HttpContextExtensions { + private const string PasswordResetFlowSessionKey = nameof(PasswordResetFlowSessionKey); + public static void SetExternalLoginProviderErrors(this HttpContext httpContext, BackOfficeExternalLoginProviderErrors errors) => httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] = errors; public static BackOfficeExternalLoginProviderErrors? GetExternalLoginProviderErrors(this HttpContext httpContext) => httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] as BackOfficeExternalLoginProviderErrors; + + internal static void StartPasswordResetFlowSession(this HttpContext httpContext, int userId) + => httpContext.Session.SetInt32(PasswordResetFlowSessionKey, userId); + + internal static void EndPasswordResetFlowSession(this HttpContext httpContext) + => httpContext.Session.Remove(PasswordResetFlowSessionKey); + + internal static bool HasActivePasswordResetFlowSession(this HttpContext httpContext, int userId) + => httpContext.Session.GetInt32(PasswordResetFlowSessionKey) == userId; }
d8f68d2c40f8Merge commit from fork
3 files changed · +28 −4
src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs+15 −4 modified@@ -132,12 +132,17 @@ public AuthenticationController( AuthorizationPolicies.BackOfficeAccess)] // Needed to enforce the principle set on the request, if one exists. public IDictionary<string, object> GetPasswordConfig(int userId) { + if (HttpContext.HasActivePasswordResetFlowSession(userId)) + { + return _passwordConfiguration.GetConfiguration(); + } + Attempt<int> currentUserId = _backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId() ?? Attempt<int>.Fail(); - return _passwordConfiguration.GetConfiguration( - currentUserId.Success - ? currentUserId.Result != userId - : true); + + return currentUserId.Success + ? _passwordConfiguration.GetConfiguration(currentUserId.Result != userId) + : new Dictionary<string, object>(); } /// <summary> @@ -345,6 +350,8 @@ public async Task<bool> IsAuthenticated() [Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)] public async Task<ActionResult<UserDetail?>> PostLogin(LoginModel loginModel) { + HttpContext.EndPasswordResetFlowSession(); + // Start a timed scope to ensure failed responses return is a consistent time await using var timedScope = new TimedScope(GetLoginDuration(), CancellationToken.None); @@ -440,6 +447,8 @@ public async Task<IActionResult> PostRequestPasswordReset(RequestPasswordResetMo return BadRequest(); } + HttpContext.EndPasswordResetFlowSession(); + BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email); await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); // To randomize response time preventing user enumeration @@ -593,6 +602,8 @@ public async Task<IActionResult> PostSend2FACode([FromBody] string provider) [AllowAnonymous] public async Task<IActionResult> PostSetPassword(SetPasswordModel model) { + HttpContext.EndPasswordResetFlowSession(); + BackOfficeIdentityUser? identityUser = await _userManager.FindByIdAsync(model.UserId.ToString(CultureInfo.InvariantCulture));
src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs+2 −0 modified@@ -370,6 +370,8 @@ public async Task<IActionResult> ValidatePasswordResetCode([Bind(Prefix = "u")] var result = await _userManager.VerifyUserTokenAsync(user, "Default", "ResetPassword", resetCode); if (result) { + HttpContext.StartPasswordResetFlowSession(userId); + //Add a flag and redirect for it to be displayed TempData[ViewDataExtensions.TokenPasswordResetCode] = _jsonSerializer.Serialize(
src/Umbraco.Web.BackOffice/Extensions/HttpContextExtensions.cs+11 −0 modified@@ -5,9 +5,20 @@ namespace Umbraco.Extensions; public static class HttpContextExtensions { + private const string PasswordResetFlowSessionKey = nameof(PasswordResetFlowSessionKey); + public static void SetExternalLoginProviderErrors(this HttpContext httpContext, BackOfficeExternalLoginProviderErrors errors) => httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] = errors; public static BackOfficeExternalLoginProviderErrors? GetExternalLoginProviderErrors(this HttpContext httpContext) => httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] as BackOfficeExternalLoginProviderErrors; + + internal static void StartPasswordResetFlowSession(this HttpContext httpContext, int userId) + => httpContext.Session.SetInt32(PasswordResetFlowSessionKey, userId); + + internal static void EndPasswordResetFlowSession(this HttpContext httpContext) + => httpContext.Session.Remove(PasswordResetFlowSessionKey); + + internal static bool HasActivePasswordResetFlowSession(this HttpContext httpContext, int userId) + => httpContext.Session.GetInt32(PasswordResetFlowSessionKey) == userId; }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-pgvc-6h2p-q4f6ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-49147ghsaADVISORY
- github.com/umbraco/Umbraco-CMS/commit/b4144564c836ec6929111ce2a12eb1f67b42d61eghsax_refsource_MISCWEB
- github.com/umbraco/Umbraco-CMS/commit/d8f68d2c40f8e158bd81d469f25ef3a4e1d86c4cghsax_refsource_MISCWEB
- github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-pgvc-6h2p-q4f6ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.