Browse Source

Merge branch 'dev' into berkan/blog-deep-dive-mcp

pull/25627/head
berkansasmaz 4 months ago
parent
commit
6ace63bfca
  1. 10
      docs/en/framework/ui/react/index.md
  2. 22
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs
  3. 16
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs
  4. 5
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs
  5. 95
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbpDynamicSortingGuard.cs
  6. 14
      framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs
  7. 4
      framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs
  8. 62
      framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/BlobContainerConfiguration_Tests.cs
  9. 14
      framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/AnotherFakeNamingNormalizer.cs
  10. 14
      framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/Fakes/FakeNamingNormalizer.cs
  11. 3
      framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg
  12. 18
      framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj
  13. 12
      framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestBase.cs
  14. 12
      framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/AbpDddApplicationTestModule.cs
  15. 277
      framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs

10
docs/en/framework/ui/react/index.md

@ -28,12 +28,12 @@ The React UI template is built with:
| [Vitest](https://vitest.dev/) | Unit testing | | [Vitest](https://vitest.dev/) | Unit testing |
| [OpenID Connect / OIDC](https://openid.net/connect/) | Authentication against the ABP Auth Server | | [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) - [View `@volo/abp-app-config` on npm](https://www.npmjs.com/package/@volo/abp-app-config)
- [`@volo/abp-oidc-auth`](https://www.npmjs.com/package/@volo/abp-oidc-auth) - [View `@volo/abp-oidc-auth` on npm](https://www.npmjs.com/package/@volo/abp-oidc-auth)
- [`@volo/abp-react-app-config`](https://www.npmjs.com/package/@volo/abp-react-app-config) - [View `@volo/abp-react-app-config` on npm](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-react-oidc-auth` on npm](https://www.npmjs.com/package/@volo/abp-react-oidc-auth)
## React App and Admin Console ## React App and Admin Console

22
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerConfiguration.cs

@ -7,10 +7,16 @@ namespace Volo.Abp.BlobStoring;
public class BlobContainerConfiguration public class BlobContainerConfiguration
{ {
private Type? _providerType;
/// <summary> /// <summary>
/// The provider to be used to store BLOBs of this container. /// The provider to be used to store BLOBs of this container.
/// </summary> /// </summary>
public Type? ProviderType { get; set; } public Type? ProviderType
{
get => _providerType ?? _fallbackConfiguration?.ProviderType;
set => _providerType = value;
}
/// <summary> /// <summary>
/// Indicates whether this container is multi-tenant or not. /// Indicates whether this container is multi-tenant or not.
@ -37,6 +43,20 @@ public class BlobContainerConfiguration
_properties = new Dictionary<string, object?>(); _properties = new Dictionary<string, object?>();
} }
/// <summary>
/// Returns the naming normalizers in effect for this container, inheriting from the fallback
/// configuration only when this container has none and does not override <see cref="ProviderType"/>.
/// </summary>
public IEnumerable<Type> GetEffectiveNamingNormalizers()
{
if (NamingNormalizers.Count == 0 && _providerType == null && _fallbackConfiguration != null)
{
return _fallbackConfiguration.GetEffectiveNamingNormalizers();
}
return NamingNormalizers;
}
public T? GetConfigurationOrDefault<T>(string name, T? defaultValue = default) public T? GetConfigurationOrDefault<T>(string name, T? defaultValue = default)
{ {
return (T?)GetConfigurationOrNull(name, defaultValue); return (T?)GetConfigurationOrNull(name, defaultValue);

16
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobNormalizeNamingService.cs

@ -19,15 +19,15 @@ public class BlobNormalizeNamingService : IBlobNormalizeNamingService, ITransien
string? containerName, string? containerName,
string? blobName) string? blobName)
{ {
var normalizerTypes = configuration.GetEffectiveNamingNormalizers();
if (!configuration.NamingNormalizers.Any()) if (!normalizerTypes.Any())
{ {
return new BlobNormalizeNaming(containerName, blobName); return new BlobNormalizeNaming(containerName, blobName);
} }
using (var scope = ServiceProvider.CreateScope()) using (var scope = ServiceProvider.CreateScope())
{ {
foreach (var normalizerType in configuration.NamingNormalizers) foreach (var normalizerType in normalizerTypes)
{ {
var normalizer = scope.ServiceProvider var normalizer = scope.ServiceProvider
.GetRequiredService(normalizerType) .GetRequiredService(normalizerType)
@ -43,21 +43,11 @@ public class BlobNormalizeNamingService : IBlobNormalizeNamingService, ITransien
public string NormalizeContainerName(BlobContainerConfiguration configuration, string containerName) public string NormalizeContainerName(BlobContainerConfiguration configuration, string containerName)
{ {
if (!configuration.NamingNormalizers.Any())
{
return containerName;
}
return NormalizeNaming(configuration, containerName, null).ContainerName!; return NormalizeNaming(configuration, containerName, null).ContainerName!;
} }
public string NormalizeBlobName(BlobContainerConfiguration configuration, string blobName) public string NormalizeBlobName(BlobContainerConfiguration configuration, string blobName)
{ {
if (!configuration.NamingNormalizers.Any())
{
return blobName;
}
return NormalizeNaming(configuration, null, blobName).BlobName!; return NormalizeNaming(configuration, null, blobName).BlobName!;
} }
} }

5
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/AbpDddApplicationModule.cs

@ -30,6 +30,11 @@ namespace Volo.Abp.Application;
)] )]
public class AbpDddApplicationModule : AbpModule public class AbpDddApplicationModule : AbpModule
{ {
public override void PreConfigureServices(ServiceConfigurationContext context)
{
AbpDynamicSortingGuard.Install();
}
public override void ConfigureServices(ServiceConfigurationContext context) public override void ConfigureServices(ServiceConfigurationContext context)
{ {
Configure<AbpApiDescriptionModelOptions>(options => Configure<AbpApiDescriptionModelOptions>(options =>

95
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;
/// <summary>
/// Framework infrastructure. Hooks <see cref="ExtensibilityPoint.QueryOptimizer"/> 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 <see cref="AbpValidationException"/>.
/// </summary>
internal static class AbpDynamicSortingGuard
{
private static readonly object InstallLock = new();
private static Func<Expression, Expression>? _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);
}
}

14
framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringOptions_Tests.cs

@ -29,10 +29,22 @@ public class AbpBlobStoringOptions_Tests : AbpBlobStoringTestBase
} }
[Fact] [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<TestContainer3>(); var config = _configurationProvider.Get<TestContainer3>();
config.ProviderType.ShouldBe(typeof(FakeBlobProvider1)); config.ProviderType.ShouldBe(typeof(FakeBlobProvider1));
config.IsMultiTenant.ShouldBeFalse();
config.GetConfigurationOrNull("TestConfigDefault").ShouldBe("TestValueDefault"); config.GetConfigurationOrNull("TestConfigDefault").ShouldBe("TestValueDefault");
} }
[Fact]
public void Should_Resolve_Fallback_Chain_Through_Configuration_Provider()
{
var testContainer1Config = _configurationProvider.Get<TestContainer1>();
testContainer1Config.IsMultiTenant.ShouldBeTrue();
var testContainer3Config = _configurationProvider.Get<TestContainer3>();
testContainer3Config.IsMultiTenant.ShouldBeFalse();
testContainer3Config.ProviderType.ShouldBe(typeof(FakeBlobProvider1));
}
} }

4
framework/test/Volo.Abp.BlobStoring.Tests/Volo/Abp/BlobStoring/AbpBlobStoringTestModule.cs

@ -36,6 +36,10 @@ public class AbpBlobStoringTestModule : AbpModule
{ {
container.SetConfiguration("TestConfig2", "TestValue2"); container.SetConfiguration("TestConfig2", "TestValue2");
container.ProviderType = typeof(FakeBlobProvider2); container.ProviderType = typeof(FakeBlobProvider2);
})
.Configure<TestContainer3>(container =>
{
container.IsMultiTenant = false;
}); });
}); });
} }

62
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<FakeNamingNormalizer>();
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<FakeNamingNormalizer>();
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<FakeNamingNormalizer>();
var namedConfig = new BlobContainerConfiguration(defaultConfig);
namedConfig.NamingNormalizers.Add<AnotherFakeNamingNormalizer>();
var effective = namedConfig.GetEffectiveNamingNormalizers().ToList();
effective.ShouldContain(typeof(AnotherFakeNamingNormalizer));
effective.ShouldNotContain(typeof(FakeNamingNormalizer));
}
}

14
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;
}
}

14
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;
}
}

3
framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.abppkg

@ -0,0 +1,3 @@
{
"role": "lib.test"
}

18
framework/test/Volo.Abp.Ddd.Application.Tests/Volo.Abp.Ddd.Application.Tests.csproj

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\..\common.test.props" />
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.Ddd.Application\Volo.Abp.Ddd.Application.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.ExceptionHandling\Volo.Abp.ExceptionHandling.csproj" />
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
</ItemGroup>
</Project>

12
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<AbpDddApplicationTestModule>
{
protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options)
{
options.UseAutofac();
}
}

12
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
{
}

277
framework/test/Volo.Abp.Ddd.Application.Tests/Volo/Abp/Application/Services/AbpDynamicSortingGuard_Tests.cs

@ -0,0 +1,277 @@
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;
namespace Volo.Abp.Application.Services;
public class AbpDynamicSortingGuard_Tests : AbpDddApplicationTestBase
{
private readonly IQueryable<FakeUser> _users;
public AbpDynamicSortingGuard_Tests()
{
_users = new List<FakeUser>
{
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<AbpValidationException>(() => _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<AbpValidationException>(() => _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<AbpValidationException>(() =>
_users.OrderBy("PasswordHash.Substring(0,1) desc").ToList());
}
finally
{
AbpDynamicSortingGuard.Reset();
AbpDynamicSortingGuard.Install();
}
}
[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<AbpValidationException>(() =>
_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<AbpValidationException>(() =>
_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<AbpValidationException>(() => _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<AbpValidationException>(() => _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<AbpValidationException>(); }
try { _users.OrderBy("").ToList(); } catch (Exception ex) { ex.ShouldNotBeOfType<AbpValidationException>(); }
try { _users.OrderBy(" ").ToList(); } catch (Exception ex) { ex.ShouldNotBeOfType<AbpValidationException>(); }
});
}
[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<AbpValidationException>(() =>
_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<AbpValidationException>(() =>
_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<AbpValidationException>(() =>
_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<AbpValidationException>(() => _users.OrderBy(sorting).ToList());
}
[Fact]
public void Should_Allow_Inherited_Property_Sorting()
{
var children = new List<FakeChildUser>
{
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; } = "";
public int Age { get; set; }
public string PasswordHash { get; set; } = "";
public FakeTenant Tenant { get; set; } = new();
}
private class FakeChildUser : FakeUser
{
public string ExtraField { get; set; } = "";
}
private class FakeTenant
{
public string Name { get; set; } = "";
}
}
Loading…
Cancel
Save