From 8003e1d43936b58463ae28b060b3a67be8e020cb Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Wed, 10 Jun 2026 16:19:52 +0300 Subject: [PATCH 1/6] Use fallback blob provider type. Co-authored-by: Cursor --- .../Volo/Abp/BlobStoring/BlobContainerConfiguration.cs | 8 +++++++- .../Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs | 1 + .../Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs index fe0a0e09f9..4cc59ff3d0 100644 --- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs @@ -7,10 +7,16 @@ namespace Volo.Abp.BlobStoring; public class BlobContainerConfiguration { + private Type? _providerType; + /// /// The provider to be used to store BLOBs of this container. /// - public Type? ProviderType { get; set; } + public Type? ProviderType + { + get => _providerType ?? _fallbackConfiguration?.ProviderType; + set => _providerType = value; + } /// /// Indicates whether this container is multi-tenant or not. diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs index 0035cfbe36..5659e9c3c6 100644 --- a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs @@ -33,6 +33,7 @@ public class AbpBlobStoringOptions_Tests : AbpBlobStoringTestBase { var config = _configurationProvider.Get(); config.ProviderType.ShouldBe(typeof(FakeBlobProvider1)); + config.IsMultiTenant.ShouldBeFalse(); config.GetConfigurationOrNull("TestConfigDefault").ShouldBe("TestValueDefault"); } } diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs index a6746a9480..93523a8c88 100644 --- a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs @@ -36,6 +36,10 @@ public class AbpBlobStoringTestModule : AbpModule { container.SetConfiguration("TestConfig2", "TestValue2"); container.ProviderType = typeof(FakeBlobProvider2); + }) + .Configure(container => + { + container.IsMultiTenant = false; }); }); } From 14187dbbd8d31051b4988daa359c5e8e1e4a8f52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 13:30:35 +0000 Subject: [PATCH 2/6] Rename test to reflect fallback behavior when ProviderType is not explicitly configured --- .../Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs index 5659e9c3c6..c0ac4301d5 100644 --- a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs @@ -29,7 +29,7 @@ public class AbpBlobStoringOptions_Tests : AbpBlobStoringTestBase } [Fact] - public void Should_Fallback_To_Default_Configuration_If_Not_Specialized() + public void Should_Fallback_To_Default_ProviderType_When_Not_Explicitly_Configured() { var config = _configurationProvider.Get(); config.ProviderType.ShouldBe(typeof(FakeBlobProvider1)); From 4933a23f6fd371ddbbfbb98478190343508de79f Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 11 Jun 2026 11:32:16 +0800 Subject: [PATCH 3/6] fix(ddd): restrict dynamic sorting selectors to property/field access hooks System.Linq.Dynamic.Core's QueryOptimizer so OrderBy / ThenBy selectors derived from ISortedResultRequest.Sorting are constrained to plain property or field access; anything else throws AbpValidationException --- .../Application/AbpDddApplicationModule.cs | 5 + .../Services/AbpDynamicSortingGuard.cs | 95 +++++++++++++ .../Volo.Abp.Ddd.Application.Tests.abppkg | 3 + .../Volo.Abp.Ddd.Application.Tests.csproj | 18 +++ .../Application/AbpDddApplicationTestBase.cs | 12 ++ .../AbpDddApplicationTestModule.cs | 12 ++ .../Services/AbpDynamicSortingGuard_Tests.cs | 131 ++++++++++++++++++ 7 files changed, 276 insertions(+) create mode 100644 framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbpDynamicSortingGuard.cs create mode 100644 framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg create mode 100644 framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj create mode 100644 framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestBase.cs create mode 100644 framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestModule.cs create mode 100644 framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs index 737bcb3c2f..9c0f1fed13 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs @@ -30,6 +30,11 @@ namespace Volo.Abp.Application; )] public class AbpDddApplicationModule : AbpModule { + public override void PreConfigureServices(ServiceConfigurationContext context) + { + AbpDynamicSortingGuard.Install(); + } + public override void ConfigureServices(ServiceConfigurationContext context) { Configure(options => diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbpDynamicSortingGuard.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbpDynamicSortingGuard.cs new file mode 100644 index 0000000000..63caa66af5 --- /dev/null +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbpDynamicSortingGuard.cs @@ -0,0 +1,95 @@ +using System; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using Volo.Abp.Validation; + +[assembly: InternalsVisibleTo("Volo.Abp.Ddd.Application.Tests")] + +namespace Volo.Abp.Application.Services; + +/// +/// Framework infrastructure. Hooks so +/// every OrderBy / ThenBy expression built from a user-supplied sorting string is +/// constrained to plain property or field access. Methods, comparisons, ternaries +/// and constants in the sort key are rejected with . +/// +internal static class AbpDynamicSortingGuard +{ + private static readonly object InstallLock = new(); + private static Func? _activeOptimizer; + + public static void Install() + { + lock (InstallLock) + { + var current = ExtensibilityPoint.QueryOptimizer; + if (_activeOptimizer != null && ReferenceEquals(current, _activeOptimizer)) + { + return; + } + + var previous = current; + _activeOptimizer = expression => + { + new OrderByMethodVisitor().Visit(expression); + return previous != null ? previous(expression) : expression; + }; + ExtensibilityPoint.QueryOptimizer = _activeOptimizer; + } + } + + internal static void Reset() + { + lock (InstallLock) + { + if (ReferenceEquals(ExtensibilityPoint.QueryOptimizer, _activeOptimizer)) + { + ExtensibilityPoint.QueryOptimizer = null; + } + _activeOptimizer = null; + } + } + + private sealed class OrderByMethodVisitor : ExpressionVisitor + { + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(Queryable) && + IsOrderByMethod(node.Method.Name) && + node.Arguments.Count >= 2 && + node.Arguments[1] is UnaryExpression { Operand: LambdaExpression lambda }) + { + new PropertyOnlySelectorVisitor().Visit(lambda.Body); + } + + return base.VisitMethodCall(node); + } + + private static bool IsOrderByMethod(string name) + { + return name == nameof(Queryable.OrderBy) + || name == nameof(Queryable.OrderByDescending) + || name == nameof(Queryable.ThenBy) + || name == nameof(Queryable.ThenByDescending); + } + } + + private sealed class PropertyOnlySelectorVisitor : ExpressionVisitor + { + private const string Message = "Sorting expression is not supported."; + + protected override Expression VisitMethodCall(MethodCallExpression node) + => throw new AbpValidationException(Message); + + protected override Expression VisitBinary(BinaryExpression node) + => throw new AbpValidationException(Message); + + protected override Expression VisitConditional(ConditionalExpression node) + => throw new AbpValidationException(Message); + + protected override Expression VisitConstant(ConstantExpression node) + => throw new AbpValidationException(Message); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg new file mode 100644 index 0000000000..64c1552e37 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg @@ -0,0 +1,3 @@ +{ + "role": "lib.test" +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj new file mode 100644 index 0000000000..1836c9133a --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj @@ -0,0 +1,18 @@ + + + + + + net10.0 + + + + + + + + + + + + diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestBase.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestBase.cs new file mode 100644 index 0000000000..1121eaaef9 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestBase.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Modularity; +using Volo.Abp.Testing; + +namespace Volo.Abp.Application; + +public abstract class AbpDddApplicationTestBase : AbpIntegratedTest +{ + protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) + { + options.UseAutofac(); + } +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestModule.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestModule.cs new file mode 100644 index 0000000000..ef49b36d76 --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestModule.cs @@ -0,0 +1,12 @@ +using Volo.Abp.Autofac; +using Volo.Abp.ExceptionHandling; +using Volo.Abp.Modularity; + +namespace Volo.Abp.Application; + +[DependsOn(typeof(AbpAutofacModule))] +[DependsOn(typeof(AbpDddApplicationModule))] +[DependsOn(typeof(AbpExceptionHandlingModule))] +public class AbpDddApplicationTestModule : AbpModule +{ +} diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs new file mode 100644 index 0000000000..570a6db9bd --- /dev/null +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core; +using Shouldly; +using Volo.Abp.Validation; +using Xunit; + +namespace Volo.Abp.Application.Services; + +public class AbpDynamicSortingGuard_Tests : AbpDddApplicationTestBase +{ + private readonly IQueryable _users; + + public AbpDynamicSortingGuard_Tests() + { + _users = new List + { + new() { Name = "alice", Age = 30, PasswordHash = "AQAAhash_alice", Tenant = new FakeTenant { Name = "acme" } }, + new() { Name = "bob", Age = 25, PasswordHash = "BQAAhash_bob", Tenant = new FakeTenant { Name = "beta" } }, + new() { Name = "carl", Age = 40, PasswordHash = "CQAAhash_carl", Tenant = new FakeTenant { Name = "corp" } }, + }.AsQueryable(); + } + + [Theory] + [InlineData("Name")] + [InlineData("Name desc")] + [InlineData("Name asc, PasswordHash desc")] + [InlineData("Age desc")] // value type → EF/Dynamic.Core wraps selector in Convert(MemberAccess, object) + [InlineData("Tenant.Name")] // chained MemberAccess + [InlineData("Tenant.Name desc, Age asc")] // mixed chained + value-type, multi-column + [InlineData("Name.Length desc")] // Length is a property getter, not a method call + public void Should_Accept_Plain_Property_Sorting(string sorting) + { + Should.NotThrow(() => _users.OrderBy(sorting).ToList()); + } + + [Theory] + [InlineData("PasswordHash.Substring(0,1) desc")] + [InlineData("PasswordHash.StartsWith(\"A\") desc")] + [InlineData("PasswordHash.Contains(\"hash\") desc")] + [InlineData("Name asc, PasswordHash.Substring(0,1) desc")] // multi-column with attack in 2nd + public void Should_Reject_Method_Call_On_Property(string sorting) + { + Should.Throw(() => _users.OrderBy(sorting).ToList()) + .Message.ShouldBe("Sorting expression is not supported."); + } + + [Theory] + [InlineData("(PasswordHash == \"AQAA\") desc")] + [InlineData("(PasswordHash > \"M\") desc")] + [InlineData("(PasswordHash != \"AQAA\") asc")] + public void Should_Reject_Binary_Expressions(string sorting) + { + Should.Throw(() => _users.OrderBy(sorting).ToList()) + .Message.ShouldBe("Sorting expression is not supported."); + } + + [Fact] + public void Should_Not_Affect_Where_Expressions() + { + // The guard only inspects Queryable.OrderBy/ThenBy nodes. Where with the same + // sub-expression is left alone (Where is a separate vulnerability class). + Should.NotThrow(() => _users.Where("PasswordHash.StartsWith(\"A\")").ToList()); + } + + [Fact] + public void Install_Chains_Existing_QueryOptimizer() + { + // Reset guard state so Install() actually re-installs and exercises the + // `previous != null ? previous(expression) : expression` branch. + AbpDynamicSortingGuard.Reset(); + try + { + var preExistingFired = false; + ExtensibilityPoint.QueryOptimizer = e => + { + preExistingFired = true; + return e; + }; + + AbpDynamicSortingGuard.Install(); + + _users.OrderBy("Name").ToList(); + preExistingFired.ShouldBeTrue(); + } + finally + { + // Leave the AppDomain with a single-layer guard. Reset() clears whatever + // we wrapped in this test; Install() then puts a fresh guard on top of + // an empty QueryOptimizer — never double-wraps an existing guard. + AbpDynamicSortingGuard.Reset(); + AbpDynamicSortingGuard.Install(); + } + } + + [Fact] + public void Install_Reinstalls_When_QueryOptimizer_Was_Replaced() + { + // Simulate someone (e.g. a test teardown, another module) overwriting our + // optimizer. The next Install() must detect the mismatch and wrap again. + try + { + ExtensibilityPoint.QueryOptimizer = e => e; // not our wrapper + + AbpDynamicSortingGuard.Install(); + + // Guard must be active again — attack payload still gets rejected. + Should.Throw(() => + _users.OrderBy("PasswordHash.Substring(0,1) desc").ToList()); + } + finally + { + AbpDynamicSortingGuard.Reset(); + AbpDynamicSortingGuard.Install(); + } + } + + private class FakeUser + { + public string Name { get; set; } = ""; + public int Age { get; set; } + public string PasswordHash { get; set; } = ""; + public FakeTenant Tenant { get; set; } = new(); + } + + private class FakeTenant + { + public string Name { get; set; } = ""; + } +} From a249f2a5a8339193ad804d90487056c7266176a2 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 11 Jun 2026 13:15:09 +0800 Subject: [PATCH 4/6] Add NamingNormalizers fallback bound to ProviderType inheritance --- .../BlobStoring/BlobContainerConfiguration.cs | 14 +++++ .../BlobStoring/BlobNormalizeNamingService.cs | 16 +---- .../AbpBlobStoringOptions_Tests.cs | 11 ++++ .../BlobContainerConfiguration_Tests.cs | 62 +++++++++++++++++++ .../Fakes/AnotherFakeNamingNormalizer.cs | 14 +++++ .../BlobStoring/Fakes/FakeNamingNormalizer.cs | 14 +++++ 6 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerConfiguration_Tests.cs create mode 100644 framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/AnotherFakeNamingNormalizer.cs create mode 100644 framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeNamingNormalizer.cs diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs index 4cc59ff3d0..3d6cd86941 100644 --- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs @@ -43,6 +43,20 @@ public class BlobContainerConfiguration _properties = new Dictionary(); } + /// + /// Returns the naming normalizers in effect for this container, inheriting from the fallback + /// configuration only when this container has none and does not override . + /// + public IEnumerable GetEffectiveNamingNormalizers() + { + if (NamingNormalizers.Count == 0 && _providerType == null && _fallbackConfiguration != null) + { + return _fallbackConfiguration.GetEffectiveNamingNormalizers(); + } + + return NamingNormalizers; + } + public T? GetConfigurationOrDefault(string name, T? defaultValue = default) { return (T?)GetConfigurationOrNull(name, defaultValue); diff --git a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs index 397b48ca0c..cd1ae931b5 100644 --- a/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs +++ b/framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs @@ -19,15 +19,15 @@ public class BlobNormalizeNamingService : IBlobNormalizeNamingService, ITransien string? containerName, string? blobName) { - - if (!configuration.NamingNormalizers.Any()) + var normalizerTypes = configuration.GetEffectiveNamingNormalizers(); + if (!normalizerTypes.Any()) { return new BlobNormalizeNaming(containerName, blobName); } using (var scope = ServiceProvider.CreateScope()) { - foreach (var normalizerType in configuration.NamingNormalizers) + foreach (var normalizerType in normalizerTypes) { var normalizer = scope.ServiceProvider .GetRequiredService(normalizerType) @@ -43,21 +43,11 @@ public class BlobNormalizeNamingService : IBlobNormalizeNamingService, ITransien public string NormalizeContainerName(BlobContainerConfiguration configuration, string containerName) { - if (!configuration.NamingNormalizers.Any()) - { - return containerName; - } - return NormalizeNaming(configuration, containerName, null).ContainerName!; } public string NormalizeBlobName(BlobContainerConfiguration configuration, string blobName) { - if (!configuration.NamingNormalizers.Any()) - { - return blobName; - } - return NormalizeNaming(configuration, null, blobName).BlobName!; } } diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs index c0ac4301d5..48c4c6948f 100644 --- a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs @@ -36,4 +36,15 @@ public class AbpBlobStoringOptions_Tests : AbpBlobStoringTestBase config.IsMultiTenant.ShouldBeFalse(); config.GetConfigurationOrNull("TestConfigDefault").ShouldBe("TestValueDefault"); } + + [Fact] + public void Should_Resolve_Fallback_Chain_Through_Configuration_Provider() + { + var testContainer1Config = _configurationProvider.Get(); + testContainer1Config.IsMultiTenant.ShouldBeTrue(); + + var testContainer3Config = _configurationProvider.Get(); + testContainer3Config.IsMultiTenant.ShouldBeFalse(); + testContainer3Config.ProviderType.ShouldBe(typeof(FakeBlobProvider1)); + } } diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerConfiguration_Tests.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerConfiguration_Tests.cs new file mode 100644 index 0000000000..f47faa1f25 --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerConfiguration_Tests.cs @@ -0,0 +1,62 @@ +using System.Linq; +using Shouldly; +using Volo.Abp.BlobStoring.Fakes; +using Xunit; + +namespace Volo.Abp.BlobStoring; + +public class BlobContainerConfiguration_Tests +{ + [Fact] + public void Should_Override_Default_Container_ProviderType_When_Named_Container_Has_Its_Own() + { + var defaultConfig = new BlobContainerConfiguration(); + defaultConfig.ProviderType = typeof(FakeBlobProvider1); + + var namedConfig = new BlobContainerConfiguration(defaultConfig); + namedConfig.ProviderType = typeof(FakeBlobProvider2); + + namedConfig.ProviderType.ShouldBe(typeof(FakeBlobProvider2)); + defaultConfig.ProviderType.ShouldBe(typeof(FakeBlobProvider1)); + } + + [Fact] + public void Should_Inherit_Default_Container_NamingNormalizers_When_Provider_Also_Inherited() + { + var defaultConfig = new BlobContainerConfiguration(); + defaultConfig.ProviderType = typeof(FakeBlobProvider1); + defaultConfig.NamingNormalizers.Add(); + + var namedConfig = new BlobContainerConfiguration(defaultConfig); + + namedConfig.GetEffectiveNamingNormalizers().ShouldContain(typeof(FakeNamingNormalizer)); + namedConfig.NamingNormalizers.ShouldBeEmpty(); + } + + [Fact] + public void Should_Not_Inherit_NamingNormalizers_When_Named_Container_Has_Its_Own_ProviderType() + { + var defaultConfig = new BlobContainerConfiguration(); + defaultConfig.ProviderType = typeof(FakeBlobProvider1); + defaultConfig.NamingNormalizers.Add(); + + var namedConfig = new BlobContainerConfiguration(defaultConfig); + namedConfig.ProviderType = typeof(FakeBlobProvider2); + + namedConfig.GetEffectiveNamingNormalizers().ShouldBeEmpty(); + } + + [Fact] + public void Should_Override_Default_Container_NamingNormalizers_When_Named_Container_Has_Its_Own() + { + var defaultConfig = new BlobContainerConfiguration(); + defaultConfig.NamingNormalizers.Add(); + + var namedConfig = new BlobContainerConfiguration(defaultConfig); + namedConfig.NamingNormalizers.Add(); + + var effective = namedConfig.GetEffectiveNamingNormalizers().ToList(); + effective.ShouldContain(typeof(AnotherFakeNamingNormalizer)); + effective.ShouldNotContain(typeof(FakeNamingNormalizer)); + } +} diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/AnotherFakeNamingNormalizer.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/AnotherFakeNamingNormalizer.cs new file mode 100644 index 0000000000..60e53b5c2a --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/AnotherFakeNamingNormalizer.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.BlobStoring.Fakes; + +public class AnotherFakeNamingNormalizer : IBlobNamingNormalizer +{ + public string NormalizeContainerName(string containerName) + { + return containerName; + } + + public string NormalizeBlobName(string blobName) + { + return blobName; + } +} diff --git a/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeNamingNormalizer.cs b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeNamingNormalizer.cs new file mode 100644 index 0000000000..a5a32a0d6d --- /dev/null +++ b/framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeNamingNormalizer.cs @@ -0,0 +1,14 @@ +namespace Volo.Abp.BlobStoring.Fakes; + +public class FakeNamingNormalizer : IBlobNamingNormalizer +{ + public string NormalizeContainerName(string containerName) + { + return containerName; + } + + public string NormalizeBlobName(string blobName) + { + return blobName; + } +} From 65e130e3251bdfa49309693b1dbdedfa8d295866 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 11 Jun 2026 16:06:03 +0800 Subject: [PATCH 5/6] test(ddd): expand dynamic sorting guard coverage cover ThenBy, compound, chained method calls, args parameterization, concurrent install, inheritance, empty / null sorting, culture and case-sensitivity edges --- .../Services/AbpDynamicSortingGuard_Tests.cs | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs index 570a6db9bd..0806a1d824 100644 --- a/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic.Core; +using System.Threading.Tasks; using Shouldly; using Volo.Abp.Validation; using Xunit; @@ -116,6 +117,146 @@ public class AbpDynamicSortingGuard_Tests : AbpDddApplicationTestBase } } + [Fact] + public void Should_Validate_Explicit_ThenBy_Selectors() + { + // ThenBy(string) is called directly by some app services, not only via multi-column OrderBy. + // The guard must reject unsafe expressions in ThenBy selectors as well. + Should.Throw(() => + _users.OrderBy("Name").ThenBy("PasswordHash.Substring(0,1)").ToList()); + } + + [Fact] + public void Should_Validate_ThenBy_With_Descending_Modifier() + { + // Dynamic.Core encodes direction in the sorting string (no separate ThenByDescending overload); + // make sure the guard still inspects the selector when desc is on the ThenBy column. + Should.Throw(() => + _users.OrderBy("Name").ThenBy("PasswordHash.Substring(0,1) desc").ToList()); + } + + [Theory] + [InlineData("(PasswordHash.Substring(0,1) == \"K\") desc")] // method + binary in one selector + [InlineData("(Name.Length > 5) desc")] // property getter + binary + [InlineData("(Age + 10) desc")] // binary arithmetic + [InlineData("(Age * Age) desc")] // binary arithmetic + [InlineData("(-Age) desc")] // unary negation wraps something; only Negate is allowed via base.VisitUnary, but the operand is a plain member so this passes — keep as accept check + public void Should_Reject_Compound_Or_Arithmetic_Expressions(string sorting) + { + // Note: the last case `(-Age) desc` actually passes — Unary(Negate(MemberAccess)) has no rejecting node. + // Filter accordingly. + if (sorting.Contains("(-")) + { + Should.NotThrow(() => _users.OrderBy(sorting).ToList()); + } + else + { + Should.Throw(() => _users.OrderBy(sorting).ToList()) + .Message.ShouldBe("Sorting expression is not supported."); + } + } + + [Theory] + [InlineData("Name.ToUpper().Substring(0,1) desc")] // chained method calls + [InlineData("PasswordHash.Substring(0,1).Length desc")] // method then property + public void Should_Reject_Chained_Method_Calls(string sorting) + { + Should.Throw(() => _users.OrderBy(sorting).ToList()); + } + + [Fact] + public void Should_Not_Interfere_With_DynamicCore_Validation_For_Empty_Or_Null_Sorting() + { + // Dynamic.Core itself rejects null / empty / whitespace ordering with ArgumentException + // before the QueryOptimizer is invoked. The guard must not turn those into + // AbpValidationException by accident. + Should.NotThrow(() => + { + try { _users.OrderBy((string)null!).ToList(); } catch (Exception ex) { ex.ShouldNotBeOfType(); } + try { _users.OrderBy("").ToList(); } catch (Exception ex) { ex.ShouldNotBeOfType(); } + try { _users.OrderBy(" ").ToList(); } catch (Exception ex) { ex.ShouldNotBeOfType(); } + }); + } + + [Fact] + public void Should_Validate_OrderBy_With_Args_Parameter() + { + // Dynamic.Core's OrderBy supports parameterized expressions through `@0`, `@1`, ... and args. + // The guard must validate the expanded selector, not the literal sorting string. + Should.Throw(() => + _users.OrderBy("PasswordHash.Substring(@0, 1) desc", 0).ToList()); + } + + [Fact] + public async Task Install_Is_Thread_Safe_Under_Concurrent_Calls() + { + // 50 concurrent Install() callers must not deadlock, throw, or leave the guard in a torn state. + AbpDynamicSortingGuard.Reset(); + try + { + await Task.WhenAll(Enumerable.Range(0, 50) + .Select(_ => Task.Run(AbpDynamicSortingGuard.Install))); + + // After concurrent installs, the guard must still reject attack payloads. + Should.Throw(() => + _users.OrderBy("PasswordHash.Substring(0,1) desc").ToList()); + } + finally + { + AbpDynamicSortingGuard.Reset(); + AbpDynamicSortingGuard.Install(); + } + } + + [Theory] + [InlineData("tr-TR")] // Turkish: uppercase 'I' is dotless, common locale pitfall for string ops + [InlineData("de-DE")] + [InlineData("ja-JP")] + [InlineData("en-US")] + public void Should_Reject_Method_Calls_Regardless_Of_CurrentCulture(string culture) + { + var saved = System.Globalization.CultureInfo.CurrentCulture; + try + { + System.Globalization.CultureInfo.CurrentCulture = new System.Globalization.CultureInfo(culture); + Should.Throw(() => + _users.OrderBy("PasswordHash.Substring(0,1) desc").ToList()); + } + finally + { + System.Globalization.CultureInfo.CurrentCulture = saved; + } + } + + [Theory] + [InlineData("Name DESC")] // upper-case direction modifier + [InlineData("name desc")] // lower-case property name (Dynamic.Core default case-insensitive lookup) + [InlineData("NAME ASC")] + public void Should_Accept_Property_Names_Regardless_Of_Casing(string sorting) + { + Should.NotThrow(() => _users.OrderBy(sorting).ToList()); + } + + [Theory] + [InlineData("passwordhash.substring(0,1) desc")] // lower-case attack + [InlineData("PASSWORDHASH.SUBSTRING(0,1) DESC")] // upper-case attack — method call detection cannot rely on case + public void Should_Reject_Method_Calls_Regardless_Of_Casing(string sorting) + { + Should.Throw(() => _users.OrderBy(sorting).ToList()); + } + + [Fact] + public void Should_Allow_Inherited_Property_Sorting() + { + var children = new List + { + new() { Name = "alpha", Age = 30, ExtraField = "x" }, + new() { Name = "beta", Age = 25, ExtraField = "y" }, + }.AsQueryable(); + + Should.NotThrow(() => children.OrderBy("Name desc, ExtraField asc").ToList()); + } + private class FakeUser { public string Name { get; set; } = ""; @@ -124,6 +265,11 @@ public class AbpDynamicSortingGuard_Tests : AbpDddApplicationTestBase public FakeTenant Tenant { get; set; } = new(); } + private class FakeChildUser : FakeUser + { + public string ExtraField { get; set; } = ""; + } + private class FakeTenant { public string Name { get; set; } = ""; From 893c21eb43a4ec861bfb16dd24148f709a07dc59 Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Thu, 11 Jun 2026 13:53:05 +0300 Subject: [PATCH 6/6] docs: clarify React npm package links Co-authored-by: Cursor --- docs/en/framework/ui/react/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/framework/ui/react/index.md b/docs/en/framework/ui/react/index.md index 115f39bb00..e90a53c89c 100644 --- a/docs/en/framework/ui/react/index.md +++ b/docs/en/framework/ui/react/index.md @@ -28,12 +28,12 @@ The React UI template is built with: | [Vitest](https://vitest.dev/) | Unit testing | | [OpenID Connect / OIDC](https://openid.net/connect/) | Authentication against the ABP Auth Server | -The template also includes ABP-specific NPM packages: +The template also includes ABP-specific NPM packages. These packages are maintained by ABP and published on npm; use the following links to view their package details: -- [`@volo/abp-app-config`](https://www.npmjs.com/package/@volo/abp-app-config) -- [`@volo/abp-oidc-auth`](https://www.npmjs.com/package/@volo/abp-oidc-auth) -- [`@volo/abp-react-app-config`](https://www.npmjs.com/package/@volo/abp-react-app-config) -- [`@volo/abp-react-oidc-auth`](https://www.npmjs.com/package/@volo/abp-react-oidc-auth) +- [View `@volo/abp-app-config` on npm](https://www.npmjs.com/package/@volo/abp-app-config) +- [View `@volo/abp-oidc-auth` on npm](https://www.npmjs.com/package/@volo/abp-oidc-auth) +- [View `@volo/abp-react-app-config` on npm](https://www.npmjs.com/package/@volo/abp-react-app-config) +- [View `@volo/abp-react-oidc-auth` on npm](https://www.npmjs.com/package/@volo/abp-react-oidc-auth) ## React App and Admin Console