mirror of https://github.com/abpframework/abp.git
committed by
GitHub
15 changed files with 328 additions and 2 deletions
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
danger=[{{ model.dangerous_action }}] |
||||
@ -0,0 +1 @@ |
|||||
|
name=[{{ model.name }}] email=[{{ model.inner.email }}] |
||||
@ -0,0 +1 @@ |
|||||
|
getType=[{{ model.GetType }}] |
||||
@ -0,0 +1 @@ |
|||||
|
loaded=[{{ model.GetType.Assembly.GetType "System.IO.File" }}] |
||||
@ -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!; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue