From 206e4a9027ce40740c5b081c6c97f6e236067458 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 23 Aug 2026 20:33:33 +0800 Subject: [PATCH 1/2] Hash the connection string in unit of work api keys The api key can be written to logs as a part of an exception message --- .../UnitOfWorkDbContextProvider.cs | 18 ++++-- .../UnitOfWorkMemoryDatabaseProvider.cs | 9 ++- .../UnitOfWorkMongoDbContextProvider.cs | 18 ++++-- .../Uow/UnitOfWorkDbContextProvider_Tests.cs | 61 ++++++++++++++++++ .../UnitOfWorkMemoryDatabaseProvider_Tests.cs | 39 ++++++++++++ .../UnitOfWorkMongoDbContextProvider_Tests.cs | 58 +++++++++++++++++ .../Abp/Uow/UnitOfWork_DatabaseApi_Tests.cs | 62 +++++++++++++++++++ 7 files changed, 255 insertions(+), 10 deletions(-) create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Uow/UnitOfWorkDbContextProvider_Tests.cs create mode 100644 framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/Uow/UnitOfWorkMemoryDatabaseProvider_Tests.cs create mode 100644 framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Uow/UnitOfWorkMongoDbContextProvider_Tests.cs create mode 100644 framework/test/Volo.Abp.Uow.Tests/Volo/Abp/Uow/UnitOfWork_DatabaseApi_Tests.cs diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs index 1ab174cc16..c8c7793f40 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs @@ -66,7 +66,7 @@ public class UnitOfWorkDbContextProvider : IDbContextProvider : IDbContextProvider : IDbContextProvider : IDbContextProvider CreateDbContextWithTransactionAsync(IUnitOfWork unitOfWork) { - var transactionApiKey = $"EntityFrameworkCore_{DbContextCreationContext.Current.ConnectionString}"; + var transactionApiKey = GetTransactionApiKey(DbContextCreationContext.Current.ConnectionString); var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as EfCoreTransactionApi; if (activeTransaction == null) @@ -351,6 +351,16 @@ public class UnitOfWorkDbContextProvider : IDbContextProvider ResolveConnectionStringAsync(string connectionStringName) { // Multi-tenancy unaware contexts should always use the host connection string diff --git a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Uow/MemoryDb/UnitOfWorkMemoryDatabaseProvider.cs b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Uow/MemoryDb/UnitOfWorkMemoryDatabaseProvider.cs index 6659bbf970..cc16da3a06 100644 --- a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Uow/MemoryDb/UnitOfWorkMemoryDatabaseProvider.cs +++ b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Uow/MemoryDb/UnitOfWorkMemoryDatabaseProvider.cs @@ -46,7 +46,7 @@ public class UnitOfWorkMemoryDatabaseProvider : IMemoryDatabas } var connectionString = _connectionStringResolver.Resolve(); - var dbContextKey = $"{typeof(TMemoryDbContext).FullName}_{connectionString}"; + var dbContextKey = GetDatabaseApiKey(connectionString); var databaseApi = unitOfWork.GetOrAddDatabaseApi( dbContextKey, @@ -66,7 +66,7 @@ public class UnitOfWorkMemoryDatabaseProvider : IMemoryDatabas } var connectionString = await _connectionStringResolver.ResolveAsync(); - var dbContextKey = $"{typeof(TMemoryDbContext).FullName}_{connectionString}"; + var dbContextKey = GetDatabaseApiKey(connectionString); var databaseApi = unitOfWork.GetOrAddDatabaseApi( dbContextKey, @@ -77,6 +77,11 @@ public class UnitOfWorkMemoryDatabaseProvider : IMemoryDatabas return ((MemoryDbDatabaseApi)databaseApi).Database; } + protected virtual string GetDatabaseApiKey(string? connectionString) + { + return $"{typeof(TMemoryDbContext).FullName}_{(connectionString ?? string.Empty).ToSha256()}"; + } + private async Task ResolveConnectionStringAsync() { // Multi-tenancy unaware contexts should always use the host connection string diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs index acea595dce..0dc3527756 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs @@ -68,7 +68,7 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext var targetDbContextType = DbContextTypeProvider.GetDbContextType(typeof(TMongoDbContext)); var connectionString = ResolveConnectionString(targetDbContextType); - var dbContextKey = $"{targetDbContextType.FullName}_{connectionString}"; + var dbContextKey = GetDatabaseApiKey(targetDbContextType, connectionString); var mongoUrl = new MongoUrl(connectionString); var databaseName = mongoUrl.DatabaseName; @@ -95,7 +95,7 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext var targetDbContextType = DbContextTypeProvider.GetDbContextType(typeof(TMongoDbContext)); var connectionString = await ResolveConnectionStringAsync(targetDbContextType); - var dbContextKey = $"{targetDbContextType.FullName}_{connectionString}"; + var dbContextKey = GetDatabaseApiKey(targetDbContextType, connectionString); var mongoUrl = new MongoUrl(connectionString); var databaseName = mongoUrl.DatabaseName; @@ -173,7 +173,7 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext MongoClient client, IMongoDatabase database) { - var transactionApiKey = $"MongoDb_{url}"; + var transactionApiKey = GetTransactionApiKey(url); var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as MongoDbTransactionApi; var dbContext = unitOfWork.ServiceProvider.GetRequiredService(); @@ -223,7 +223,7 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext IMongoDatabase database, CancellationToken cancellationToken = default) { - var transactionApiKey = $"MongoDb_{url}"; + var transactionApiKey = GetTransactionApiKey(url); var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as MongoDbTransactionApi; var dbContext = unitOfWork.ServiceProvider.GetRequiredService(); @@ -266,6 +266,16 @@ public class UnitOfWorkMongoDbContextProvider : IMongoDbContext return dbContext; } + protected virtual string GetDatabaseApiKey(Type dbContextType, string? connectionString) + { + return $"{dbContextType.FullName}_{(connectionString ?? string.Empty).ToSha256()}"; + } + + protected virtual string GetTransactionApiKey(MongoUrl url) + { + return $"MongoDb_{url.ToString().ToSha256()}"; + } + protected virtual async Task ResolveConnectionStringAsync(Type dbContextType) { // Multi-tenancy unaware contexts should always use the host connection string diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Uow/UnitOfWorkDbContextProvider_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Uow/UnitOfWorkDbContextProvider_Tests.cs new file mode 100644 index 0000000000..86964f8de2 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Uow/UnitOfWorkDbContextProvider_Tests.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.Uow; + +public class UnitOfWorkDbContextProvider_Tests : EntityFrameworkCoreTestBase +{ + private readonly IPersonRepository _personRepository; + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IConnectionStringResolver _connectionStringResolver; + + public UnitOfWorkDbContextProvider_Tests() + { + _personRepository = GetRequiredService(); + _unitOfWorkManager = GetRequiredService(); + _connectionStringResolver = GetRequiredService(); + } + + [Fact] + public async Task Should_Not_Use_Connection_String_As_Database_Api_Key() + { + var connectionString = await _connectionStringResolver.ResolveAsync( + ConnectionStringNameAttribute.GetConnStringName() + ); + + using (var uow = _unitOfWorkManager.Begin()) + { + await _personRepository.GetDbContextAsync(); + + uow.FindDatabaseApi($"{typeof(TestAppDbContext).FullName}_{connectionString}").ShouldBeNull(); + uow.FindDatabaseApi($"{typeof(TestAppDbContext).FullName}_{connectionString.ToSha256()}").ShouldNotBeNull(); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Should_Not_Use_Connection_String_As_Transaction_Api_Key() + { + var connectionString = await _connectionStringResolver.ResolveAsync( + ConnectionStringNameAttribute.GetConnStringName() + ); + + using (var uow = _unitOfWorkManager.Begin(new AbpUnitOfWorkOptions { IsTransactional = true })) + { + await _personRepository.GetDbContextAsync(); + + uow.FindTransactionApi($"EntityFrameworkCore_{connectionString}").ShouldBeNull(); + uow.FindTransactionApi($"EntityFrameworkCore_{connectionString.ToSha256()}").ShouldNotBeNull(); + + await uow.CompleteAsync(); + } + } +} diff --git a/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/Uow/UnitOfWorkMemoryDatabaseProvider_Tests.cs b/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/Uow/UnitOfWorkMemoryDatabaseProvider_Tests.cs new file mode 100644 index 0000000000..33fb06c997 --- /dev/null +++ b/framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/Uow/UnitOfWorkMemoryDatabaseProvider_Tests.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.TestApp.MemoryDb; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.MemoryDb.Uow; + +public class UnitOfWorkMemoryDatabaseProvider_Tests : MemoryDbTestBase +{ + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IConnectionStringResolver _connectionStringResolver; + private readonly IMemoryDatabaseProvider _memoryDatabaseProvider; + + public UnitOfWorkMemoryDatabaseProvider_Tests() + { + _unitOfWorkManager = GetRequiredService(); + _connectionStringResolver = GetRequiredService(); + _memoryDatabaseProvider = GetRequiredService>(); + } + + [Fact] + public async Task Should_Not_Use_Connection_String_As_Database_Api_Key() + { + var connectionString = await _connectionStringResolver.ResolveAsync(); + + using (var uow = _unitOfWorkManager.Begin()) + { + await _memoryDatabaseProvider.GetDatabaseAsync(); + + uow.FindDatabaseApi($"{typeof(TestAppMemoryDbContext).FullName}_{connectionString}").ShouldBeNull(); + uow.FindDatabaseApi($"{typeof(TestAppMemoryDbContext).FullName}_{connectionString.ToSha256()}").ShouldNotBeNull(); + + await uow.CompleteAsync(); + } + } +} diff --git a/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Uow/UnitOfWorkMongoDbContextProvider_Tests.cs b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Uow/UnitOfWorkMongoDbContextProvider_Tests.cs new file mode 100644 index 0000000000..3fe400e9e8 --- /dev/null +++ b/framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Uow/UnitOfWorkMongoDbContextProvider_Tests.cs @@ -0,0 +1,58 @@ +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.TestApp.MongoDB; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.MongoDB.Uow; + +[Collection(MongoTestCollection.Name)] +public class UnitOfWorkMongoDbContextProvider_Tests : MongoDbTestBase +{ + private readonly IUnitOfWorkManager _unitOfWorkManager; + private readonly IConnectionStringResolver _connectionStringResolver; + private readonly IMongoDbContextProvider _mongoDbContextProvider; + + public UnitOfWorkMongoDbContextProvider_Tests() + { + _unitOfWorkManager = GetRequiredService(); + _connectionStringResolver = GetRequiredService(); + _mongoDbContextProvider = GetRequiredService>(); + } + + [Fact] + public async Task Should_Not_Use_Connection_String_As_Database_Api_Key() + { + var connectionString = await _connectionStringResolver.ResolveAsync(); + + using (var uow = _unitOfWorkManager.Begin()) + { + await _mongoDbContextProvider.GetDbContextAsync(); + + uow.FindDatabaseApi($"{typeof(TestAppMongoDbContext).FullName}_{connectionString}").ShouldBeNull(); + uow.FindDatabaseApi($"{typeof(TestAppMongoDbContext).FullName}_{connectionString.ToSha256()}").ShouldNotBeNull(); + + await uow.CompleteAsync(); + } + } + + [Fact] + public async Task Should_Not_Use_Connection_String_As_Transaction_Api_Key() + { + var connectionString = await _connectionStringResolver.ResolveAsync(); + var mongoUrl = new MongoUrl(connectionString); + + using (var uow = _unitOfWorkManager.Begin(new AbpUnitOfWorkOptions { IsTransactional = true })) + { + await _mongoDbContextProvider.GetDbContextAsync(); + + uow.FindTransactionApi($"MongoDb_{mongoUrl}").ShouldBeNull(); + uow.FindTransactionApi($"MongoDb_{mongoUrl.ToString().ToSha256()}").ShouldNotBeNull(); + + await uow.CompleteAsync(); + } + } +} diff --git a/framework/test/Volo.Abp.Uow.Tests/Volo/Abp/Uow/UnitOfWork_DatabaseApi_Tests.cs b/framework/test/Volo.Abp.Uow.Tests/Volo/Abp/Uow/UnitOfWork_DatabaseApi_Tests.cs new file mode 100644 index 0000000000..8493440d47 --- /dev/null +++ b/framework/test/Volo.Abp.Uow.Tests/Volo/Abp/Uow/UnitOfWork_DatabaseApi_Tests.cs @@ -0,0 +1,62 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Uow; + +public class UnitOfWork_DatabaseApi_Tests : AbpIntegratedTest +{ + private const string KeyWithSecret = "MyDbContext_Server=localhost;User Id=sa;Password=SecretMarker;"; + + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public UnitOfWork_DatabaseApi_Tests() + { + _unitOfWorkManager = GetRequiredService(); + } + + [Fact] + public void Should_Not_Expose_Database_Api_Key_On_Duplicate() + { + using (var uow = _unitOfWorkManager.Begin()) + { + uow.AddDatabaseApi(KeyWithSecret, new FakeDatabaseApi()); + + var exception = Assert.Throws(() => uow.AddDatabaseApi(KeyWithSecret, new FakeDatabaseApi())); + exception.ToString().ShouldNotContain("SecretMarker"); + } + } + + [Fact] + public void Should_Not_Expose_Transaction_Api_Key_On_Duplicate() + { + using (var uow = _unitOfWorkManager.Begin()) + { + uow.AddTransactionApi(KeyWithSecret, new FakeTransactionApi()); + + var exception = Assert.Throws(() => uow.AddTransactionApi(KeyWithSecret, new FakeTransactionApi())); + exception.ToString().ShouldNotContain("SecretMarker"); + } + } + + private class FakeDatabaseApi : IDatabaseApi + { + + } + + private class FakeTransactionApi : ITransactionApi + { + public Task CommitAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void Dispose() + { + + } + } +} From 3945a9145982b3a99b3d6e046070ce2269c20369 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 23 Aug 2026 20:33:39 +0800 Subject: [PATCH 2/2] Use HashData and ToHexString in string hash extensions --- .../System/AbpStringExtensions.cs | 12 ++++++++++ .../System/StringExtensions_Tests.cs | 24 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs b/framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs index 48f628d438..a8ce242871 100644 --- a/framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs +++ b/framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs @@ -384,6 +384,7 @@ public static class AbpStringExtensions public static string ToMd5(this string str) { +#if NETSTANDARD2_0 || NETSTANDARD2_1 using (var md5 = MD5.Create()) { var inputBytes = Encoding.UTF8.GetBytes(str); @@ -397,10 +398,14 @@ public static class AbpStringExtensions return sb.ToString(); } +#else + return Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(str))); +#endif } public static string ToSha256(this string str) { +#if NETSTANDARD2_0 || NETSTANDARD2_1 using (var sha = SHA256.Create()) { var data = sha.ComputeHash(Encoding.UTF8.GetBytes(str)); @@ -412,10 +417,14 @@ public static class AbpStringExtensions } return sb.ToString(); } +#else + return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(str))).ToLowerInvariant(); +#endif } public static string ToSha512(this string str) { +#if NETSTANDARD2_0 || NETSTANDARD2_1 using (var sha = SHA512.Create()) { var data = sha.ComputeHash(Encoding.UTF8.GetBytes(str)); @@ -427,6 +436,9 @@ public static class AbpStringExtensions } return sb.ToString(); } +#else + return Convert.ToHexString(SHA512.HashData(Encoding.UTF8.GetBytes(str))).ToLowerInvariant(); +#endif } /// diff --git a/framework/test/Volo.Abp.Core.Tests/System/StringExtensions_Tests.cs b/framework/test/Volo.Abp.Core.Tests/System/StringExtensions_Tests.cs index 1b53d1b7d7..42612f776c 100644 --- a/framework/test/Volo.Abp.Core.Tests/System/StringExtensions_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/System/StringExtensions_Tests.cs @@ -255,6 +255,30 @@ public class StringExtensions_Tests : IDisposable Encoding.ASCII.GetString(bytes).ShouldBe(str); } + [Theory] + [InlineData("", "D41D8CD98F00B204E9800998ECF8427E")] + [InlineData("abc", "900150983CD24FB0D6963F7D28E17F72")] + public void ToMd5_Test(string str, string expected) + { + str.ToMd5().ShouldBe(expected); + } + + [Theory] + [InlineData("", "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e")] + [InlineData("abc", "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f")] + public void ToSha512_Test(string str, string expected) + { + str.ToSha512().ShouldBe(expected); + } + + [Theory] + [InlineData("", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData("abc", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad")] + public void ToSha256_Test(string str, string expected) + { + str.ToSha256().ShouldBe(expected); + } + private enum MyEnum { MyValue1,