Browse Source

Merge pull request #25407 from abpframework/auto-merge/rel-10-4/4561

Merge branch dev with rel-10.4
pull/25416/head
Volosoft Agent 3 months ago
committed by GitHub
parent
commit
ac2fd2f06f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      docs/en/framework/infrastructure/text-templating/razor.md
  2. 25
      docs/en/framework/infrastructure/text-templating/scriban.md
  3. 16
      docs/en/modules/text-template-management.md
  4. 81
      docs/en/release-info/migration-guides/abp-10-4.md
  5. 7
      framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs
  6. 2
      framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs
  7. 2
      framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs
  8. 18
      framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs
  9. 29
      framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs
  10. 1
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl
  11. 1
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl
  12. 1
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl
  13. 1
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl
  14. 123
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs
  15. 21
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs

2
docs/en/framework/infrastructure/text-templating/razor.md

@ -10,6 +10,8 @@
The Razor template is a standard C# class, so you can freely use the functions of C#, such as `dependency injection`, using `LINQ`, custom methods, and even using `Repository`.
> The Razor engine compiles template content into a fully-trusted .NET assembly via Roslyn and executes it in the host process, so editing a Razor template at runtime is functionally equivalent to executing arbitrary server-side code. `RazorTemplateRenderingEngine.IsSandboxed` is therefore `false`, and the [Text Template Management](../../../modules/text-template-management.md) module requires the `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` permission (in addition to `EditContents`) before allowing such templates to be edited via its UI. Grant the related permission only to fully trusted developers/operators. If you need a sandboxed engine for content editors, consider [Scriban](scriban.md), which is configured to honor Scriban's [safe runtime boundaries](https://github.com/scriban/scriban/blob/master/site/docs/runtime/safe-runtime.md) by default.
## Installation

25
docs/en/framework/infrastructure/text-templating/scriban.md

@ -7,6 +7,31 @@
# Scriban Integration
## Safe Runtime (Sandbox)
Scriban's [safe runtime](https://github.com/scriban/scriban/blob/master/site/docs/runtime/safe-runtime.md) builds the practical sandbox out of four boundaries: which globals you expose through `ScriptObject`, which .NET members you allow through the member filter, whether you configure `TemplateContext.TemplateLoader` for `include`, and which `TemplateContext` execution limits you enable. ABP's `ScribanTemplateRenderingEngine` is configured to honor these boundaries by default:
| Boundary | ABP default |
|----------|-------------|
| Globals exposed | Only the `globalContext` (`Dictionary<string, object>`) entries, the `model` you pass to `RenderAsync`, and the `L` localization helper. |
| .NET member access | `TemplateContext.MemberFilter` is set to `IsMemberAllowed`, an allowlist that exposes public properties only. Methods, fields, events, and `object`-level members (`GetType`, `ToString`, ...) are not reachable, which closes reflection-based escape paths such as `{{ model.GetType.Assembly.GetType "..." }}`. |
| `TemplateLoader` | Not configured. `include` directives have no template loader and cannot read templates from disk or other sources unless you explicitly wire one up. |
| Execution limits | Scriban's defaults (`LoopLimit = 1000`, `RecursiveLimit = 100`, `LimitToString = 1 MB`, `RegexTimeOut = 10s`). Override `CreateScribanTemplateContext` to tighten these for your own scenarios. |
The recommended way to expose data to a Scriban template is via `ScriptObject` or `IDictionary<string, object>` — the keys you put there are exactly what the template can see. When you pass a .NET object as `model`, the `MemberFilter` ensures only properties are exposed, but the safest pattern is to pre-build a dictionary or `ScriptObject` so the surface is fully under your control:
````csharp
await _templateRenderer.RenderAsync(
"MyTemplate",
model: new Dictionary<string, object>
{
{ "name", user.Name },
{ "email", user.Email }
});
````
If you must pass a .NET object whose methods/fields the template needs to read, override `ScribanTemplateRenderingEngine.IsMemberAllowed` to relax the filter. Only do so when the model objects are trusted and do not carry secrets, since methods and reflection entry points become reachable to whoever can edit the template content.
## Installation
It is suggested to use the [ABP CLI](../../../cli) to install this package.

16
docs/en/modules/text-template-management.md

@ -164,6 +164,22 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do
See the `TextTemplateManagementPermissions` class members for all permissions defined for this module.
The module exposes two edit-time permissions with different risk levels:
| Permission | Required to edit | Default grant |
|------------|------------------|---------------|
| `TextTemplateManagement.TextTemplates.EditContents` | Templates rendered by a sandboxed engine (e.g. Scriban). Editing such templates is safe for content editors because the engine cannot execute arbitrary .NET code. | Granted to roles that need to edit template text. |
| `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` | Templates rendered by a **non-sandboxed** engine (e.g. Razor). Editing such templates is functionally equivalent to granting server-side code execution because the engine compiles the content into a .NET assembly that runs with the host process's privileges. | **Not granted to any role by default**, including `admin`. Must be granted explicitly. |
Whether a template is sandboxed is determined by `ITemplateRenderingEngine.IsSandboxed` on the engine that renders it. Editing a non-sandboxed template requires **both** `EditContents` and `EditNonSandboxedContents`.
The Text Template Management UI surfaces this distinction:
- A warning banner is rendered above the editor for non-sandboxed templates.
- The save and restore buttons are disabled when the current user lacks `EditNonSandboxedContents` for a non-sandboxed template.
> Treat `EditNonSandboxedContents` as equivalent to granting shell access to the application server. Only assign it to fully trusted developers or operators.
### Angular UI

81
docs/en/release-info/migration-guides/abp-10-4.md

@ -141,6 +141,87 @@ Configure<AbpPhoneNumberTwoFactorTokenProviderOptions>(options =>
> See [#25235](https://github.com/abpframework/abp/pull/25235) for details.
### Text Template Rendering Engine — `IsSandboxed` Marker
**Who is affected**
- Custom rendering engines that implement `Volo.Abp.TextTemplating.ITemplateRenderingEngine` directly (not deriving from `TemplateRenderingEngineBase`).
- Modules and applications that surface template editing to non-developer users (e.g. the Text Template Management module).
**What changed**
- `ITemplateRenderingEngine` exposes a new required property:
```csharp
bool IsSandboxed { get; }
```
Sandboxed engines (e.g. Scriban) interpret templates as a restricted DSL without .NET interop. Non-sandboxed engines (e.g. Razor) compile templates into fully-trusted .NET code that runs with the same privileges as the host process.
- `TemplateRenderingEngineBase` provides a virtual default of `false` (secure-by-default): engines that derive from the base class and don't override the property are treated as non-sandboxed.
- `RazorTemplateRenderingEngine` declares `IsSandboxed => false` (compiles to .NET assembly via Roslyn).
- `ScribanTemplateRenderingEngine` declares `IsSandboxed => true` and now sets Scriban's `TemplateContext.MemberFilter` so only public properties on imported objects are exposed; methods, fields, events and reflection entry points (`GetType`, `Assembly`, ...) are no longer reachable from Scriban templates.
**What to do**
- If your application registers a custom engine by implementing `ITemplateRenderingEngine` directly (without deriving from `TemplateRenderingEngineBase`), add the property:
```csharp
public bool IsSandboxed => false; // or true if your engine cannot execute host code
```
- If your engine derives from `TemplateRenderingEngineBase`, no action is required for compilation; however, override `IsSandboxed => true` if your engine is genuinely sandboxed so callers (such as the Text Template Management module) treat its templates as safe to edit by non-developer users.
- Scriban templates that invoke methods on the model (e.g. `{{ model.SomeMethod }}`) or read fields stop working because the engine now whitelists public properties only. Templates that access only properties (the typical Scriban usage) are unaffected. To restore the previous behavior in custom hosts, derive from `ScribanTemplateRenderingEngine` and override `IsMemberAllowed` to allow methods or fields:
```csharp
protected override bool IsMemberAllowed(MemberInfo member) => true;
```
Only do this when the model objects are trusted and do not carry secrets, since the previous behavior exposed reflection entry points (`GetType`, `Assembly`, ...) on imported .NET objects.
> See [#25399](https://github.com/abpframework/abp/pull/25399) for details.
### Text Template Management — `EditContents` Permission Split (security)
**Who is affected**
- Applications using the Text Template Management module that have granted the `TextTemplateManagement.TextTemplates.EditContents` permission to roles that are not fully trusted server administrators/developers.
- In particular, applications that use Razor templates (default for solutions referencing `Volo.Abp.TextTemplating.Razor`) and have any non-developer role with `EditContents`.
**What changed**
- A new permission `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` has been added.
- Editing a template whose rendering engine is **non-sandboxed** (i.e. `ITemplateRenderingEngine.IsSandboxed == false`, e.g. Razor) now requires **both** permissions: `EditContents` and `EditNonSandboxedContents`.
- The new permission is **not granted to any role by default**, including the `admin` role. The previously implicit assumption — that `EditContents` was enough to edit Razor templates — has been corrected: editing such templates is functionally equivalent to granting server-side code execution and is now gated by an explicit permission whose name communicates that risk.
- The Text Template Management UI (MVC, Blazor) renders a security warning banner when the current template's engine is non-sandboxed, and disables the save/restore buttons when the current user lacks the new permission.
**What to do**
After upgrading, audit which roles currently hold `EditContents` and decide which of them should also be granted `EditNonSandboxedContents`:
1. Roles that should only edit sandboxed templates (e.g. Scriban, Liquid, plain HTML) need no further action — they keep editing those templates.
2. Roles that need to continue editing Razor templates must be granted `EditNonSandboxedContents` explicitly via the Permission Management page.
3. If — and only if — you have reviewed your role assignments and confirmed that every role currently holding `EditContents` is trusted to execute server-side code through Razor templates, you may seed the new permission for those roles via your `IDataSeedContributor`:
```csharp
var grants = await _permissionGrantRepository.GetListAsync(
TextTemplateManagementPermissions.TextTemplates.EditContents);
foreach (var grant in grants)
{
await _permissionManager.SetAsync(
TextTemplateManagementPermissions.TextTemplates.EditNonSandboxedContents,
grant.ProviderName,
grant.ProviderKey,
isGranted: true);
}
```
> Do not automate this seeding for arbitrary tenants/roles without first reviewing the current grants — auto-restoring the previously-implicit elevated trust would defeat the security improvement. The recommended path is to grant the new permission only to specific developer/operator roles via the Permission Management UI.
If your application's data seeder grants all permissions in the `TextTemplateManagement` group to a role (e.g. the `admin` role), that role will automatically receive `EditNonSandboxedContents` on first run after upgrade. Audit your seeder if you want stricter defaults.
> See the [Razor Integration](../../framework/infrastructure/text-templating/razor.md) document and [#25399](https://github.com/abpframework/abp/pull/25399) for details.
### Dependency Updates
**Who is affected**

7
framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs

@ -9,6 +9,13 @@ public interface ITemplateRenderingEngine
{
string Name { get; }
/// <summary>
/// True when this engine restricts templates to a DSL without direct .NET
/// interop (e.g. Scriban). False when templates compile to fully-trusted
/// .NET code (e.g. Razor).
/// </summary>
bool IsSandboxed { get; }
/// <summary>
/// Renders a text template.
/// </summary>

2
framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs

@ -8,6 +8,8 @@ public abstract class TemplateRenderingEngineBase : ITemplateRenderingEngine
{
public abstract string Name { get; }
public virtual bool IsSandboxed => false;
protected readonly ITemplateDefinitionManager TemplateDefinitionManager;
protected readonly ITemplateContentProvider TemplateContentProvider;
protected readonly IStringLocalizerFactory StringLocalizerFactory;

2
framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs

@ -17,6 +17,8 @@ public class RazorTemplateRenderingEngine : TemplateRenderingEngineBase, ITransi
public const string EngineName = "Razor";
public override string Name => EngineName;
public override bool IsSandboxed => false;
protected readonly IServiceScopeFactory ServiceScopeFactory;
public RazorTemplateRenderingEngine(

18
framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Localization;
@ -14,6 +15,8 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran
public const string EngineName = "Scriban";
public override string Name => EngineName;
public override bool IsSandboxed => true;
public ScribanTemplateRenderingEngine(
ITemplateDefinitionManager templateDefinitionManager,
ITemplateContentProvider templateContentProvider,
@ -118,7 +121,10 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran
Dictionary<string, object> globalContext,
object? model = null)
{
var context = new TemplateContext();
var context = new TemplateContext
{
MemberFilter = IsMemberAllowed
};
var scriptObject = new ScriptObject();
@ -140,4 +146,12 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran
return context;
}
/// <summary>
/// Scriban member filter: only public properties on imported objects are exposed.
/// </summary>
protected virtual bool IsMemberAllowed(MemberInfo member)
{
return member is PropertyInfo;
}
}

29
framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs

@ -0,0 +1,29 @@
using Shouldly;
using Xunit;
namespace Volo.Abp.TextTemplating.Razor;
public class RazorTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatingTestBase<RazorTextTemplatingTestModule>
{
private readonly RazorTemplateRenderingEngine _engine;
public RazorTemplateRenderingEngine_IsSandboxed_Tests()
{
_engine = GetRequiredService<RazorTemplateRenderingEngine>();
}
[Fact]
public void Razor_Engine_Should_Not_Be_Sandboxed()
{
// Razor templates compile into fully-trusted .NET code; editing them is
// equivalent to granting server-side code execution.
_engine.IsSandboxed.ShouldBeFalse();
}
[Fact]
public void Razor_Engine_Should_Expose_IsSandboxed_Through_Interface()
{
ITemplateRenderingEngine asInterface = _engine;
asInterface.IsSandboxed.ShouldBeFalse();
}
}

1
framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl

@ -0,0 +1 @@
danger=[{{ model.dangerous_action }}]

1
framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl

@ -0,0 +1 @@
name=[{{ model.name }}] email=[{{ model.inner.email }}]

1
framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl

@ -0,0 +1 @@
getType=[{{ model.GetType }}]

1
framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl

@ -0,0 +1 @@
loaded=[{{ model.GetType.Assembly.GetType "System.IO.File" }}]

123
framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs

@ -0,0 +1,123 @@
using System.Threading.Tasks;
using global::Scriban.Syntax;
using Shouldly;
using Xunit;
namespace Volo.Abp.TextTemplating.Scriban;
public class ScribanTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatingTestBase<ScribanTextTemplatingTestModule>
{
private readonly ScribanTemplateRenderingEngine _engine;
private readonly ITemplateRenderer _templateRenderer;
public ScribanTemplateRenderingEngine_IsSandboxed_Tests()
{
_engine = GetRequiredService<ScribanTemplateRenderingEngine>();
_templateRenderer = GetRequiredService<ITemplateRenderer>();
}
[Fact]
public void Scriban_Engine_Should_Be_Sandboxed()
{
// Scriban interprets templates as a restricted DSL without .NET interop;
// editing template content is safe for non-developer users.
_engine.IsSandboxed.ShouldBeTrue();
}
[Fact]
public void Scriban_Engine_Should_Expose_IsSandboxed_Through_Interface()
{
ITemplateRenderingEngine asInterface = _engine;
asInterface.IsSandboxed.ShouldBeTrue();
}
[Fact]
public async Task Should_Hide_GetType_Member_On_Imported_Model()
{
// The engine projects .NET model objects into a ScriptObject containing
// only the model's public readable properties. Reflection entry points
// such as object.GetType are not part of the projection, so
// "{{ model.GetType }}" silently resolves to null (empty output) instead
// of leaking the runtime type.
var result = await _templateRenderer.RenderAsync(
ScribanTestTemplateDefinitionProvider.ReflectionEscapeAttempt,
model: new ReflectionEscapeModel { Name = "John" });
result.ShouldBe("getType=[]\n");
result.ShouldNotContain("RuntimeType");
result.ShouldNotContain("Volo.Abp.TextTemplating");
}
[Fact]
public async Task Should_Block_Reflection_Escape_Chain()
{
// Direct reflection chain that an attacker would attempt:
// {{ model.GetType.Assembly.GetType "System.IO.File" }}
// Because the projected ScriptObject does not expose GetType, the first
// hop yields null and Scriban raises a ScriptRuntimeException when the
// chain dereferences a null. Surfacing an error is desirable: silent
// failure could mask escape attempts.
var ex = await Should.ThrowAsync<ScriptRuntimeException>(async () =>
await _templateRenderer.RenderAsync(
ScribanTestTemplateDefinitionProvider.ReflectionEscapeChain,
model: new ReflectionEscapeModel { Name = "John" }));
// The error must reference the broken chain (null), not a leaked Type
// or Assembly value.
ex.Message.ShouldContain("null");
ex.Message.ShouldNotContain("System.IO.File");
ex.Message.ShouldNotContain("System.Private.CoreLib");
}
[Fact]
public async Task Should_Hide_Methods_Of_Imported_Model()
{
// Methods on .NET objects are not exposed by the projection so
// attackers cannot invoke side-effecting methods (e.g. repository
// mutators) even when the host accidentally imports a service-like
// object as a model.
var result = await _templateRenderer.RenderAsync(
ScribanTestTemplateDefinitionProvider.MethodInvocationAttempt,
model: new ServiceLikeModel());
result.ShouldBe("danger=[]\n");
result.ShouldNotContain("UNSAFE");
}
[Fact]
public async Task Should_Project_Properties_Including_Nested_Objects()
{
// The projection must still expose user-defined readable properties
// (and recurse into nested objects) so legitimate template usage keeps
// working after sandboxing.
var result = await _templateRenderer.RenderAsync(
ScribanTestTemplateDefinitionProvider.NestedPropertyAccess,
model: new OuterModel { Name = "John", Inner = new InnerModel { Email = "j@a.b" } });
result.ShouldBe("name=[John] email=[j@a.b]\n");
}
private class ReflectionEscapeModel
{
public string Name { get; set; } = default!;
}
private class ServiceLikeModel
{
public string DangerousAction()
{
return "UNSAFE";
}
}
private class OuterModel
{
public string Name { get; set; } = default!;
public InnerModel Inner { get; set; } = default!;
}
private class InnerModel
{
public string Email { get; set; } = default!;
}
}

21
framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs

@ -2,6 +2,11 @@
public class ScribanTestTemplateDefinitionProvider : TemplateDefinitionProvider
{
public const string ReflectionEscapeAttempt = "ReflectionEscapeAttempt";
public const string ReflectionEscapeChain = "ReflectionEscapeChain";
public const string MethodInvocationAttempt = "MethodInvocationAttempt";
public const string NestedPropertyAccess = "NestedPropertyAccess";
public override void Define(ITemplateDefinitionContext context)
{
context.GetOrNull(TestTemplates.WelcomeEmail)?
@ -19,5 +24,21 @@ public class ScribanTestTemplateDefinitionProvider : TemplateDefinitionProvider
context.GetOrNull(TestTemplates.ShowDecimalNumber)?
.WithVirtualFilePath("/SampleTemplates/ShowDecimalNumber.tpl", true)
.WithScribanEngine();
context.Add(new TemplateDefinition(ReflectionEscapeAttempt)
.WithVirtualFilePath("/SampleTemplates/ReflectionEscapeAttempt.tpl", true)
.WithScribanEngine());
context.Add(new TemplateDefinition(ReflectionEscapeChain)
.WithVirtualFilePath("/SampleTemplates/ReflectionEscapeChain.tpl", true)
.WithScribanEngine());
context.Add(new TemplateDefinition(MethodInvocationAttempt)
.WithVirtualFilePath("/SampleTemplates/MethodInvocationAttempt.tpl", true)
.WithScribanEngine());
context.Add(new TemplateDefinition(NestedPropertyAccess)
.WithVirtualFilePath("/SampleTemplates/NestedPropertyAccess.tpl", true)
.WithScribanEngine());
}
}

Loading…
Cancel
Save