From e7774523de07185012914e9c5ac91ce48ecd9c85 Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 10 May 2026 12:06:13 +0800 Subject: [PATCH 1/2] Add IsSandboxed to ITemplateRenderingEngine ITemplateRenderingEngine exposes a new IsSandboxed property so callers can decide whether editing a template requires elevated trust. - TemplateRenderingEngineBase provides a virtual default of false (secure-by-default) - RazorTemplateRenderingEngine declares IsSandboxed=false (compiles to .NET assembly via Roslyn) - ScribanTemplateRenderingEngine declares IsSandboxed=true (DSL without .NET interop) - Razor integration docs and TextTemplateManagement docs document the implications - Migration guide for ABP 10.4 documents the new abstraction member --- .../infrastructure/text-templating/razor.md | 8 ++ docs/en/modules/text-template-management.md | 16 ++++ .../release-info/migration-guides/abp-10-4.md | 74 +++++++++++++++++++ .../ITemplateRenderingEngine.cs | 20 +++++ .../TemplateRenderingEngineBase.cs | 3 + .../Razor/RazorTemplateRenderingEngine.cs | 9 +++ .../Scriban/ScribanTemplateRenderingEngine.cs | 9 +++ ...mplateRenderingEngine_IsSandboxed_Tests.cs | 29 ++++++++ ...mplateRenderingEngine_IsSandboxed_Tests.cs | 29 ++++++++ 9 files changed, 197 insertions(+) create mode 100644 framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs create mode 100644 framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs diff --git a/docs/en/framework/infrastructure/text-templating/razor.md b/docs/en/framework/infrastructure/text-templating/razor.md index d2f690c72c..917006c7bd 100644 --- a/docs/en/framework/infrastructure/text-templating/razor.md +++ b/docs/en/framework/infrastructure/text-templating/razor.md @@ -10,6 +10,14 @@ 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`. +> ⚠ **Security notice** +> +> The Razor rendering engine compiles template content into a fully-trusted .NET assembly via Roslyn and executes it in the host process. **Editing a Razor template at runtime is functionally equivalent to executing arbitrary server-side code** — the template can access the filesystem, environment variables, the application's DI container, secrets, and any other .NET API. +> +> The framework reflects this fact through the `ITemplateRenderingEngine.IsSandboxed` property: `RazorTemplateRenderingEngine.IsSandboxed == false`. The [Text Template Management](../../../modules/text-template-management.md) module reads this flag and requires the dedicated `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` permission (in addition to `EditContents`) before allowing such templates to be edited via its UI. +> +> Treat the ability to edit Razor template content as equivalent to granting shell access to the application server, and grant the related permission only to fully trusted developers/operators. If you need a sandboxed engine for content editors, consider [Scriban](scriban.md), whose templates cannot invoke arbitrary .NET APIs. + ## Installation diff --git a/docs/en/modules/text-template-management.md b/docs/en/modules/text-template-management.md index fa62ee7470..32b7cc31a9 100644 --- a/docs/en/modules/text-template-management.md +++ b/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 diff --git a/docs/en/release-info/migration-guides/abp-10-4.md b/docs/en/release-info/migration-guides/abp-10-4.md index 213d8b7230..9087f3b493 100644 --- a/docs/en/release-info/migration-guides/abp-10-4.md +++ b/docs/en/release-info/migration-guides/abp-10-4.md @@ -141,6 +141,80 @@ Configure(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` (DSL with no .NET interop). + +**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. + +> See [#XXXXX](https://github.com/abpframework/abp/pull/XXXXX) 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 [#XXXXX](https://github.com/abpframework/abp/pull/XXXXX) for details. + ### Dependency Updates **Who is affected** diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs index e100825011..51e57027b6 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs @@ -9,6 +9,26 @@ public interface ITemplateRenderingEngine { string Name { get; } + /// + /// Indicates whether this engine renders template content in a sandboxed way that + /// prevents the content from accessing the host runtime (filesystem, environment, + /// arbitrary .NET APIs, etc.). + /// + /// Sandboxed engines (e.g. Scriban, Liquid) 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. + /// + /// + /// Implementations are required to declare this explicitly. The recommended + /// secure-by-default value is false: any engine that doesn't have a clear + /// sandboxing story should return false so callers such as the + /// TextTemplateManagement module treat its templates as requiring elevated trust + /// to edit. provides a virtual default + /// of false for engines deriving from it. + /// + /// + bool IsSandboxed { get; } + /// /// Renders a text template. /// diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs index 62c8dee43c..1c01854f33 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs +++ b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs @@ -8,6 +8,9 @@ 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; diff --git a/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs index 485ca44121..58d1b5df96 100644 --- a/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs @@ -17,6 +17,15 @@ public class RazorTemplateRenderingEngine : TemplateRenderingEngineBase, ITransi public const string EngineName = "Razor"; public override string Name => EngineName; + /// + /// + /// Razor templates are compiled into .NET assemblies via Roslyn and executed in the + /// host process with full access to the BCL and DI container. They are NOT sandboxed, + /// and editing template contents is functionally equivalent to granting server-side + /// code execution. + /// + public override bool IsSandboxed => false; + protected readonly IServiceScopeFactory ServiceScopeFactory; public RazorTemplateRenderingEngine( diff --git a/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs index 7a5ac79592..58a70ea806 100644 --- a/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs @@ -14,6 +14,15 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran public const string EngineName = "Scriban"; public override string Name => EngineName; + /// + /// + /// Scriban interprets templates as a restricted DSL without direct .NET interop. + /// Templates cannot invoke arbitrary BCL types unless explicitly imported into the + /// script object by the host, so editing template content is safe for non-developer + /// users. + /// + public override bool IsSandboxed => true; + public ScribanTemplateRenderingEngine( ITemplateDefinitionManager templateDefinitionManager, ITemplateContentProvider templateContentProvider, diff --git a/framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs b/framework/test/Volo.Abp.TextTemplating.Razor.Tests/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine_IsSandboxed_Tests.cs new file mode 100644 index 0000000000..aba81af6f3 --- /dev/null +++ b/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 +{ + private readonly RazorTemplateRenderingEngine _engine; + + public RazorTemplateRenderingEngine_IsSandboxed_Tests() + { + _engine = GetRequiredService(); + } + + [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(); + } +} diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs new file mode 100644 index 0000000000..a82f7cc038 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs @@ -0,0 +1,29 @@ +using Shouldly; +using Xunit; + +namespace Volo.Abp.TextTemplating.Scriban; + +public class ScribanTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatingTestBase +{ + private readonly ScribanTemplateRenderingEngine _engine; + + public ScribanTemplateRenderingEngine_IsSandboxed_Tests() + { + _engine = GetRequiredService(); + } + + [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(); + } +} From 33157ec8e726ab5a6fcfb9dad2fb9afb1768b04e Mon Sep 17 00:00:00 2001 From: maliming Date: Sun, 10 May 2026 13:56:59 +0800 Subject: [PATCH 2/2] Address review feedback on IsSandboxed - Soften IsSandboxed XML doc as a best-effort marker - Replace #XXXXX placeholders with #25399 - Set Scriban MemberFilter to allowlist public properties only, blocking method/field access and reflection escape paths - Update Razor and Scriban safe-runtime docs to match - Add reflection-escape, method-invocation and nested-property tests for Scriban --- .../infrastructure/text-templating/razor.md | 8 +- .../infrastructure/text-templating/scriban.md | 25 +++++ .../release-info/migration-guides/abp-10-4.md | 15 ++- .../ITemplateRenderingEngine.cs | 19 +--- .../TemplateRenderingEngineBase.cs | 1 - .../Razor/RazorTemplateRenderingEngine.cs | 7 -- .../Scriban/ScribanTemplateRenderingEngine.cs | 23 +++-- .../MethodInvocationAttempt.tpl | 1 + .../SampleTemplates/NestedPropertyAccess.tpl | 1 + .../ReflectionEscapeAttempt.tpl | 1 + .../SampleTemplates/ReflectionEscapeChain.tpl | 1 + ...mplateRenderingEngine_IsSandboxed_Tests.cs | 94 +++++++++++++++++++ .../ScribanTestTemplateDefinitionProvider.cs | 21 +++++ 13 files changed, 173 insertions(+), 44 deletions(-) create mode 100644 framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl create mode 100644 framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl create mode 100644 framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl create mode 100644 framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl diff --git a/docs/en/framework/infrastructure/text-templating/razor.md b/docs/en/framework/infrastructure/text-templating/razor.md index 917006c7bd..a8bc1c55a6 100644 --- a/docs/en/framework/infrastructure/text-templating/razor.md +++ b/docs/en/framework/infrastructure/text-templating/razor.md @@ -10,13 +10,7 @@ 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`. -> ⚠ **Security notice** -> -> The Razor rendering engine compiles template content into a fully-trusted .NET assembly via Roslyn and executes it in the host process. **Editing a Razor template at runtime is functionally equivalent to executing arbitrary server-side code** — the template can access the filesystem, environment variables, the application's DI container, secrets, and any other .NET API. -> -> The framework reflects this fact through the `ITemplateRenderingEngine.IsSandboxed` property: `RazorTemplateRenderingEngine.IsSandboxed == false`. The [Text Template Management](../../../modules/text-template-management.md) module reads this flag and requires the dedicated `TextTemplateManagement.TextTemplates.EditNonSandboxedContents` permission (in addition to `EditContents`) before allowing such templates to be edited via its UI. -> -> Treat the ability to edit Razor template content as equivalent to granting shell access to the application server, and grant the related permission only to fully trusted developers/operators. If you need a sandboxed engine for content editors, consider [Scriban](scriban.md), whose templates cannot invoke arbitrary .NET APIs. +> 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 diff --git a/docs/en/framework/infrastructure/text-templating/scriban.md b/docs/en/framework/infrastructure/text-templating/scriban.md index bb72624294..0748f859d7 100644 --- a/docs/en/framework/infrastructure/text-templating/scriban.md +++ b/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`) 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` — 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 + { + { "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. diff --git a/docs/en/release-info/migration-guides/abp-10-4.md b/docs/en/release-info/migration-guides/abp-10-4.md index 9087f3b493..ff3718336c 100644 --- a/docs/en/release-info/migration-guides/abp-10-4.md +++ b/docs/en/release-info/migration-guides/abp-10-4.md @@ -159,7 +159,7 @@ Configure(options => 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` (DSL with no .NET interop). +- `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** @@ -170,8 +170,15 @@ Configure(options => ``` - 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: -> See [#XXXXX](https://github.com/abpframework/abp/pull/XXXXX) for details. + ```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) @@ -209,11 +216,11 @@ After upgrading, audit which roles currently hold `EditContents` and decide whic } ``` -> ⚠ 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. +> 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 [#XXXXX](https://github.com/abpframework/abp/pull/XXXXX) for details. +> See the [Razor Integration](../../framework/infrastructure/text-templating/razor.md) document and [#25399](https://github.com/abpframework/abp/pull/25399) for details. ### Dependency Updates diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs index 51e57027b6..e9f74595c8 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs @@ -10,22 +10,9 @@ public interface ITemplateRenderingEngine string Name { get; } /// - /// Indicates whether this engine renders template content in a sandboxed way that - /// prevents the content from accessing the host runtime (filesystem, environment, - /// arbitrary .NET APIs, etc.). - /// - /// Sandboxed engines (e.g. Scriban, Liquid) 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. - /// - /// - /// Implementations are required to declare this explicitly. The recommended - /// secure-by-default value is false: any engine that doesn't have a clear - /// sandboxing story should return false so callers such as the - /// TextTemplateManagement module treat its templates as requiring elevated trust - /// to edit. provides a virtual default - /// of false for engines deriving from it. - /// + /// 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). /// bool IsSandboxed { get; } diff --git a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs index 1c01854f33..7e5e10d86c 100644 --- a/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs +++ b/framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs @@ -8,7 +8,6 @@ public abstract class TemplateRenderingEngineBase : ITemplateRenderingEngine { public abstract string Name { get; } - /// public virtual bool IsSandboxed => false; protected readonly ITemplateDefinitionManager TemplateDefinitionManager; diff --git a/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs index 58d1b5df96..b6c120fe9e 100644 --- a/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs +++ b/framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs @@ -17,13 +17,6 @@ public class RazorTemplateRenderingEngine : TemplateRenderingEngineBase, ITransi public const string EngineName = "Razor"; public override string Name => EngineName; - /// - /// - /// Razor templates are compiled into .NET assemblies via Roslyn and executed in the - /// host process with full access to the BCL and DI container. They are NOT sandboxed, - /// and editing template contents is functionally equivalent to granting server-side - /// code execution. - /// public override bool IsSandboxed => false; protected readonly IServiceScopeFactory ServiceScopeFactory; diff --git a/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs b/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs index 58a70ea806..9b7b3393a6 100644 --- a/framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs +++ b/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,13 +15,6 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran public const string EngineName = "Scriban"; public override string Name => EngineName; - /// - /// - /// Scriban interprets templates as a restricted DSL without direct .NET interop. - /// Templates cannot invoke arbitrary BCL types unless explicitly imported into the - /// script object by the host, so editing template content is safe for non-developer - /// users. - /// public override bool IsSandboxed => true; public ScribanTemplateRenderingEngine( @@ -127,7 +121,10 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran Dictionary globalContext, object? model = null) { - var context = new TemplateContext(); + var context = new TemplateContext + { + MemberFilter = IsMemberAllowed + }; var scriptObject = new ScriptObject(); @@ -149,4 +146,12 @@ public class ScribanTemplateRenderingEngine : TemplateRenderingEngineBase, ITran return context; } + + /// + /// Scriban member filter: only public properties on imported objects are exposed. + /// + protected virtual bool IsMemberAllowed(MemberInfo member) + { + return member is PropertyInfo; + } } diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl new file mode 100644 index 0000000000..9666a9e986 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl @@ -0,0 +1 @@ +danger=[{{ model.dangerous_action }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl new file mode 100644 index 0000000000..14eb44cce2 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl @@ -0,0 +1 @@ +name=[{{ model.name }}] email=[{{ model.inner.email }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl new file mode 100644 index 0000000000..6ccfcfcef6 --- /dev/null +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl @@ -0,0 +1 @@ +getType=[{{ model.GetType }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl new file mode 100644 index 0000000000..ada402ac57 --- /dev/null +++ b/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" }}] diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs index a82f7cc038..2aa0cd018f 100644 --- a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs +++ b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs @@ -1,3 +1,5 @@ +using System.Threading.Tasks; +using global::Scriban.Syntax; using Shouldly; using Xunit; @@ -6,10 +8,12 @@ namespace Volo.Abp.TextTemplating.Scriban; public class ScribanTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatingTestBase { private readonly ScribanTemplateRenderingEngine _engine; + private readonly ITemplateRenderer _templateRenderer; public ScribanTemplateRenderingEngine_IsSandboxed_Tests() { _engine = GetRequiredService(); + _templateRenderer = GetRequiredService(); } [Fact] @@ -26,4 +30,94 @@ public class ScribanTemplateRenderingEngine_IsSandboxed_Tests : AbpTextTemplatin 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(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!; + } } diff --git a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs b/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs index 0087288c32..006f2db450 100644 --- a/framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs +++ b/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()); } }