Hi,
Thanks for the update, glad to hear you have an internal procedure in place for now. The fix is already merged and will ship in the next release, so you can drop the workaround once you upgrade.
Thanks
Hi,
Found the root cause. In your NotificationController.cs:
[HttpGet("/{id}")]
public Task<NotificationDto> GetAsync(Guid id)
{
return _notificationAppService.GetAsync(id);
}
The leading / in [HttpGet("/{id}")] makes the route absolute, which overrides the controller-level [Route("api/app/notification")]. As a result this action matches any single-segment URL at the root — including /home, /about, etc.
Since Emerald.Web.Public depends on EmeraldHttpApiModule, this controller is registered in the Public website's routing table. When your HomeController redirects unauthenticated users to /home, ASP.NET Core matches NotificationController.GetAsync, tries to bind home to the Guid id parameter, fails, and returns 400. The [Authorize] attribute on the controller is also what triggers the AuthServer redirect you observed.
The log confirms this:
Executing endpoint 'Emerald.Controllers.Notifications.NotificationController.GetAsync (Emerald.HttpApi)'
Route matched with {area = "app", controller = "Notification", action = "Get"}. Executing controller action with signature ... GetAsync(System.Guid) ...
Request finished HTTP/2 GET https://localhost:44362/home - 400
Fix: Remove the leading / so the route becomes relative to the controller:
[HttpGet("{id}")]
public Task<NotificationDto> GetAsync(Guid id)
{
return _notificationAppService.GetAsync(id);
}
The endpoint will then be GET /api/app/notification/{id} as intended.
Thanks
Hi,
Confirmed — with the default ABP setup you're running, your app isn't hit by CVE-2026-40372. ABP does not call UseDataProtection() on the OpenIddict server/validation builder, so all OpenIddict tokens (auth code, refresh, device code, state) are encrypted with the X.509 certificate you register via AddEncryptionCertificate(...), and access tokens are JWTs (DisableAccessTokenEncryption() is applied by default). The DP flaw only reaches the ASP.NET Core side of your app (cookies, antiforgery, ASP.NET Core Identity DP tokens), not OpenIddict tokens.
On top of that, Azure App Service Linux has already rolled the shared framework to 10.0.7+, which is what your framework-dependent hosts actually load for Microsoft.AspNetCore.DataProtection. So in practice you're already on the patched assembly.
Per-question answers below.
1. Transitive exposure through ABP packages
ABP core and Volo.Abp.OpenIddict.* don't pull Microsoft.AspNetCore.DataProtection as a NuGet reference on net10.0 — on that TFM, OpenIddict.*.AspNetCore gets DataProtection from the Microsoft.AspNetCore.App shared framework. The only NuGet-level DP refs in ABP templates are Microsoft.AspNetCore.DataProtection.StackExchangeRedis and, on the Pro side, a direct Microsoft.AspNetCore.DataProtection reference in Volo.Abp.Identity.Pro.Domain and Volo.FileManagement.Domain.
Loading rule is max(sharedFramework, PackageReference), and it's safe iff that max is ≥ 10.0.7. As long as your runtime patch is 10.0.7+, the shared framework wins and you're safe even with the current 10.0.2 PackageReference in ABP 10.2.
We already pushed PRs on April 23 to bump all Microsoft.* / System.* packages to 10.0.7 across ABP, ABP Pro, and ABP Studio, so you'll pick that up in the next 10.2.x / 10.3.x patch. If you want a hard floor now without waiting for the bump, add these to each host csproj — this guarantees NuGet wins even if the runtime patch somewhere lags:
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="10.0.7" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="10.0.7" />
2. Recommended revocation pattern for tiered solutions
Your plan is exactly what we'd recommend: one-shot IHostedService on the AuthServer, gated by a config flag. The AuthServer owns the key-ring registration and the OpenIddict domain services, so both IKeyManager.RevokeAllKeys() and IOpenIddictTokenManager.RevokeAsync(null, null, null, null) belong there. ABP doesn't have a dedicated idiomatic hook for this; IHostedService or OnApplicationInitializationAsync both work. Use a DB row / Redis key as an idempotency marker so it doesn't re-run on every restart.
Sketch:
public class CveKeyRevocationHostedService : IHostedService
{
private readonly IServiceProvider _serviceProvider;
private readonly IConfiguration _configuration;
public CveKeyRevocationHostedService(IServiceProvider serviceProvider, IConfiguration configuration)
{
_serviceProvider = serviceProvider;
_configuration = configuration;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!_configuration.GetValue<bool>("CabMD:DataProtection:RevokeOnStartup"))
{
return;
}
using var scope = _serviceProvider.CreateScope();
var keyManager = scope.ServiceProvider.GetRequiredService<IKeyManager>();
keyManager.RevokeAllKeys(DateTimeOffset.UtcNow, "CVE-2026-40372");
var tokenManager = scope.ServiceProvider.GetRequiredService<IOpenIddictTokenManager>();
await tokenManager.RevokeAsync(subject: null, client: null, status: null, type: null);
// persist an idempotency marker here (DB row / Redis key) so a later restart skips this
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
Register it in the AuthServer module's ConfigureServices:
context.Services.AddHostedService<CveKeyRevocationHostedService>();
3. ABP-internal consumers of Data Protection
Here's the full list for an ABP tiered setup. Everything below is invalidated by RevokeAllKeys — but none of it stores DP-protected ciphertext in the database, so revocation only forces re-auth and invalidates pending email/share links. It does not break any at-rest data.
state parameter (Google/Microsoft/etc. handlers)AbpSingleActiveTokenProvider — stores a SHA256 hash in AbpUserTokens, not the DP ciphertext, so revoke is safeUserInvitationTokenProvider — pending invitation emails become invalidShareFileTokenProvider in File Management — any outstanding share link becomes invalidNot affected: all OpenIddict tokens (auth code, refresh, device code, state, access token) — they use the OpenIddict encryption/signing certificate, not DP.
4. OpenIddict encryption/signing certificate vs. DP key ring
Correct — they're completely separate. The certificate registered via AddEncryptionCertificate / AddSigningCertificate on OpenIddictServerBuilder encrypts OpenIddict's own tokens. The DP key ring is only used by ASP.NET Core (cookies, antiforgery) and ASP.NET Core Identity DP token providers. CVE-2026-40372 is purely about the DP encryptor HMAC flaw, so you do not need to rotate the OpenIddict encryption/signing cert for this CVE.
5. SetApplicationName interop after revocation
There is a per-host cache. KeyRingProvider caches the active key ring, and KeyManagementOptions.KeyRingRefreshPeriod controls refresh (default 24h). RevokeAllKeys does not push a notification to other hosts — they'll only pick up the revocation and the new active key on their next refresh.
That means there is a transient window after you call RevokeAllKeys where the other hosts are still holding the stale key ring: unprotect will fail (keys revoked), and protect operations on those nodes can race when auto-generating a new active key.
Coordinated restart of all hosts (AuthServer + HttpApi.Host + Web + Web.Public + Hangfire) immediately after revocation is the right call. Don't rely on the 24h refresh for incident response.
Recommended sequence for your deployment window:
CabMD:DataProtection:RevokeOnStartup=true on the AuthServer only.RevokeAllKeys + RevokeAsync.Thanks.
Hi,
Thanks for catching this during testing — really appreciate it, that kind of feedback before production saves us a lot of pain. The fix will land in the next release, so you can grab it from there.
Thanks
Hi,
You're right, this is a problem. The invite-accept path didn't run the global username/email uniqueness check in shared mode, so two different invitees could each claim admin in their tenant. Host AbpUsers ended up with duplicate rows and login can only resolve the first one, which is why the second tenant's admin can't sign in.
Until the fix ships in 10.2.2 / 10.3.1 you can either give each tenant's admin a unique username (e.g. admin-tenant1 / admin-tenant2), or drop this UserSharingManager override into your Domain module to reject the duplicate right now:
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(UserSharingManager))]
public class MyUserSharingManager : UserSharingManager
{
public MyUserSharingManager(
IOptions<AbpMultiTenancyOptions> multiTenancyOptions,
IDataFilter dataFilter,
ITenantStore tenantStore,
IdentityProUserManager userManager,
IIdentityUserRepository userRepository,
IdentityRoleManager roleManager,
IIdentityRoleRepository roleRepository,
UserInvitationManager userInvitationManager,
IAbpDistributedLock distributedLock)
: base(multiTenancyOptions, dataFilter, tenantStore, userManager, userRepository, roleManager, roleRepository, userInvitationManager, distributedLock)
{
}
public override async Task AcceptInviteAsync(Guid invitationId, string username, string password, string passwordHash)
{
if (IsEnabled())
{
var invitation = await UserInvitationManager.GetAsync(invitationId);
if (invitation.Status == UserInvitationStatus.Pending)
{
var hostUsersWithUserName = await GetUsersByUserNameFromHostAsync(username, includeLeaved: true);
if (hostUsersWithUserName.Any(x => !string.Equals(x.Email, invitation.InviteeEmail, StringComparison.OrdinalIgnoreCase)))
{
throw new BusinessException(IdentityProErrorCodes.InviteUserUserNameAlreadyExists).WithData("UserName", username);
}
var hostUsersWithEmail = await GetUsersByEmailFromHostAsync(invitation.InviteeEmail, includeLeaved: true);
if (hostUsersWithEmail.Any(x => !string.Equals(x.UserName, username, StringComparison.OrdinalIgnoreCase)))
{
throw new BusinessException(IdentityProErrorCodes.InviteUserEmailAlreadyExists).WithData("Email", invitation.InviteeEmail);
}
}
}
await base.AcceptInviteAsync(invitationId, username, password, passwordHash);
}
}
If you already have duplicate rows in host AbpUsers from testing, delete one or rename its UserName so login can disambiguate.
Thanks
Hi,
Could you share a few things so we can reproduce this on our side?
HttpApi.Host, Web.Public(.Host) and Blazor.WebApp.Tieredhome CMS page was created (is home the slug, or did you set it as the default home page?)PagesFeature via GlobalFeatureManagerHttpApi.Host (port 44362 in your log) and the Web.Public(.Host) when the issue happens. Here is how to enable Debug logs: https://abp.io/support/questions/8622/How-to-enable-Debug-logs-for-troubleshoot-problemsIf possible, a minimal reproduction project would speed things up a lot.
liming.ma@volosoft.com
Thanks
Hi,
That's actually intentional. In shared mode, users are global across host and all tenants, so things like activate / deactivate / lock / 2FA / delete only make sense at the host level. That's why the tenant admin UI hides those buttons — it's not a permission issue on your side.
To remove someone from just your tenant, they can Leave from their own account menu. A full delete or deactivate needs a host admin to do it.
Thanks
Hi,
For per-tenant module control, you want Feature Management, not Permission Management. Permission = what a role is allowed to do; Feature = what a tenant/edition can use. Your case is the latter, and ABP has this OOTB.
Here's the full flow:
1. Define your features
public class MyAppFeatureDefinitionProvider : FeatureDefinitionProvider
{
public override void Define(IFeatureDefinitionContext context)
{
var group = context.AddGroup("MyApp", L("Features:MyApp"));
group.AddFeature(
"MyApp.HomeServices",
defaultValue: "false",
displayName: L("Features:HomeServices"),
valueType: new ToggleStringValueType());
group.AddFeature(
"MyApp.BusinessServices",
defaultValue: "false",
displayName: L("Features:BusinessServices"),
valueType: new ToggleStringValueType());
}
private static LocalizableString L(string name)
=> LocalizableString.Create<MyAppResource>(name);
}
2. Protect the application services
[RequiresFeature("MyApp.HomeServices")]
public class HomeServicesAppService : ApplicationService, IHomeServicesAppService
{
// only reachable when the feature is enabled for the current tenant
}
Or inline inside a method:
await FeatureChecker.CheckEnabledAsync("MyApp.HomeServices");
3. Hide the menu item for tenants that don't have it
context.Menu.AddItem(
new ApplicationMenuItem("HomeServices", l["Menu:HomeServices"], "/home-services")
.RequireFeatures("MyApp.HomeServices"));
4. Assign per tenant — no code needed
Login as host admin, go to Saas → Tenants → (the tenant) → Actions → Manage Features, toggle Home Services ON for Tenant A and leave Business Services OFF. Save. Tenant A now only sees/uses Home Services.
About the "all disabled" permission screen your tenant admin sees — that's unrelated to the module toggle. It just means the logged-in tenant admin user doesn't have PermissionManagement.ManagePermissions granted, so the grid is read-only. But for module-level per-tenant control, features are the right tool, not permissions.
Docs:
Thanks
Hi,
Confirmed, the dropdowns on the AuditLogs Clear button don't refresh because LeptonX wraps the <select> with a custom UI that doesn't yet react to programmatic value changes.
Until the next LeptonX patch is out, you can add this override on the page to reset the visible label manually:
$(function () {
$("#ClearFilterButton").on('click', function () {
$("#HttpMethod, #HttpStatusCode, #HasException").each(function () {
this.value = "";
var wrapper = this.closest(".custom-select-wrapper");
if (wrapper) {
var display = wrapper.querySelector(".custom-select-display span");
var selected = this.options[this.selectedIndex];
if (display && selected) {
display.textContent = selected.textContent;
}
}
});
});
});
This runs after the built-in AuditLogs/index.js (which already clears the text fields and reloads the table) and works regardless of the LeptonX version you're on. Once we release the Volo.Abp.AuditLogging fix and LeptonX ships the matching patch, upgrading both packages and re-running abp install-libs will remove the need for this override.
Thanks
Hi,
Both advisories are fixed in lodash 4.18.0+. @abp/lodash already allows any 4.x ("lodash": "^4.17.21"), so you can force the safe version in your own project right now:
package.json (use overrides for npm/pnpm):{
"resolutions": {
"lodash": "^4.18.1"
}
}
yarn install
abp install-libs
abp install-libs copies the physical files from node_modules into wwwroot/libs, so once lodash is bumped there, your wwwroot/libs/lodash/lodash.min.js will be the patched version and both advisories should clear.
We'll also raise the floor in @abp/lodash for future releases.
Thanks