Browse Source

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
pull/25399/head
maliming 5 months ago
parent
commit
33157ec8e7
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 8
      docs/en/framework/infrastructure/text-templating/razor.md
  2. 25
      docs/en/framework/infrastructure/text-templating/scriban.md
  3. 15
      docs/en/release-info/migration-guides/abp-10-4.md
  4. 19
      framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/ITemplateRenderingEngine.cs
  5. 1
      framework/src/Volo.Abp.TextTemplating.Core/Volo/Abp/TextTemplating/TemplateRenderingEngineBase.cs
  6. 7
      framework/src/Volo.Abp.TextTemplating.Razor/Volo/Abp/TextTemplating/Razor/RazorTemplateRenderingEngine.cs
  7. 23
      framework/src/Volo.Abp.TextTemplating.Scriban/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine.cs
  8. 1
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/MethodInvocationAttempt.tpl
  9. 1
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/NestedPropertyAccess.tpl
  10. 1
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeAttempt.tpl
  11. 1
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/SampleTemplates/ReflectionEscapeChain.tpl
  12. 94
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTemplateRenderingEngine_IsSandboxed_Tests.cs
  13. 21
      framework/test/Volo.Abp.TextTemplating.Scriban.Tests/Volo/Abp/TextTemplating/Scriban/ScribanTestTemplateDefinitionProvider.cs

8
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

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.

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

@ -159,7 +159,7 @@ Configure<AbpPhoneNumberTwoFactorTokenProviderOptions>(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<AbpPhoneNumberTwoFactorTokenProviderOptions>(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

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

@ -10,22 +10,9 @@ public interface ITemplateRenderingEngine
string Name { get; }
/// <summary>
/// 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.).
/// <para>
/// 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.
/// </para>
/// <para>
/// Implementations are required to declare this explicitly. The recommended
/// secure-by-default value is <c>false</c>: any engine that doesn't have a clear
/// sandboxing story should return <c>false</c> so callers such as the
/// TextTemplateManagement module treat its templates as requiring elevated trust
/// to edit. <see cref="TemplateRenderingEngineBase"/> provides a virtual default
/// of <c>false</c> for engines deriving from it.
/// </para>
/// 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; }

1
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; }
/// <inheritdoc cref="ITemplateRenderingEngine.IsSandboxed" />
public virtual bool IsSandboxed => false;
protected readonly ITemplateDefinitionManager TemplateDefinitionManager;

7
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;
/// <inheritdoc />
/// <remarks>
/// 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.
/// </remarks>
public override bool IsSandboxed => false;
protected readonly IServiceScopeFactory ServiceScopeFactory;

23
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;
/// <inheritdoc />
/// <remarks>
/// 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.
/// </remarks>
public override bool IsSandboxed => true;
public ScribanTemplateRenderingEngine(
@ -127,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();
@ -149,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;
}
}

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" }}]

94
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<ScribanTextTemplatingTestModule>
{
private readonly ScribanTemplateRenderingEngine _engine;
private readonly ITemplateRenderer _templateRenderer;
public ScribanTemplateRenderingEngine_IsSandboxed_Tests()
{
_engine = GetRequiredService<ScribanTemplateRenderingEngine>();
_templateRenderer = GetRequiredService<ITemplateRenderer>();
}
[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<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