mirror of
https://github.com/semihalev/twig.git
synced 2026-03-14 13:55:46 +01:00
- Implement SecurityPolicy interface with function/filter/tag restrictions - Add DefaultSecurityPolicy with sensible defaults for common operations - Add sandboxed option to include tag for secure template inclusion - Implement context-level sandbox flag and methods - Add engine-level sandbox control methods - Create comprehensive tests for sandbox functionality 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
277 lines
7.7 KiB
Go
277 lines
7.7 KiB
Go
package twig
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// StringLoader is a simple template loader that delegates to engine's registered templates
|
|
type StringLoader struct {
|
|
templates map[string]string
|
|
}
|
|
|
|
// Load implements the Loader interface and returns a template by name
|
|
func (l *StringLoader) Load(name string) (string, error) {
|
|
// The engine already has the templates registered via RegisterString
|
|
// This is just a dummy implementation to satisfy the interface
|
|
// The actual template loading is handled by the engine's internal cache
|
|
return "", fmt.Errorf("template not found: '%s'", name)
|
|
}
|
|
|
|
// Exists implements the Loader interface
|
|
func (l *StringLoader) Exists(name string) bool {
|
|
// Always return false to let the engine load from its internal cache
|
|
return false
|
|
}
|
|
|
|
// TestExtension is a simple extension for testing
|
|
type TestExtension struct {
|
|
functions map[string]FunctionFunc
|
|
filters map[string]FilterFunc
|
|
}
|
|
|
|
func (e *TestExtension) GetName() string {
|
|
return "test_extension"
|
|
}
|
|
|
|
func (e *TestExtension) GetFilters() map[string]FilterFunc {
|
|
return e.filters
|
|
}
|
|
|
|
func (e *TestExtension) GetFunctions() map[string]FunctionFunc {
|
|
return e.functions
|
|
}
|
|
|
|
func (e *TestExtension) GetTests() map[string]TestFunc {
|
|
return nil
|
|
}
|
|
|
|
func (e *TestExtension) GetOperators() map[string]OperatorFunc {
|
|
return nil
|
|
}
|
|
|
|
func (e *TestExtension) GetTokenParsers() []TokenParser {
|
|
return nil
|
|
}
|
|
|
|
func (e *TestExtension) Initialize(engine *Engine) {
|
|
// Nothing to initialize
|
|
}
|
|
|
|
// TestSandboxFunctions tests if the sandbox can restrict function access
|
|
func TestSandboxFunctions(t *testing.T) {
|
|
// Create a fresh engine
|
|
engine := New()
|
|
|
|
// Create a default security policy that doesn't allow any functions
|
|
policy := NewDefaultSecurityPolicy()
|
|
policy.AllowedFunctions = map[string]bool{} // Start with no allowed functions
|
|
|
|
// Register a test function through a custom extension
|
|
engine.AddExtension(&TestExtension{
|
|
functions: map[string]FunctionFunc{
|
|
"test_func": func(args ...interface{}) (interface{}, error) {
|
|
return "test function called", nil
|
|
},
|
|
},
|
|
})
|
|
|
|
// Enable sandbox mode with the restrictive policy
|
|
engine.EnableSandbox(policy)
|
|
|
|
// Register a template that uses the function
|
|
err := engine.RegisterString("sandbox_test", "{{ test_func() }}")
|
|
if err != nil {
|
|
t.Fatalf("Error registering template: %v", err)
|
|
}
|
|
|
|
// Render in sandbox mode (should fail)
|
|
ctx := NewRenderContext(engine.environment, nil, engine)
|
|
ctx.EnableSandbox() // Enable sandbox mode explicitly in context
|
|
|
|
// Try to render
|
|
var buf bytes.Buffer
|
|
template, err := engine.Load("sandbox_test")
|
|
if err != nil {
|
|
t.Fatalf("Error loading template: %v", err)
|
|
}
|
|
|
|
// Rendering should fail because the function is not allowed
|
|
err = template.nodes.Render(&buf, ctx)
|
|
if err == nil {
|
|
t.Errorf("Expected sandbox to block unauthorized function, but it didn't")
|
|
} else {
|
|
t.Logf("Correctly got error: %v", err)
|
|
}
|
|
|
|
// Now allow the function and try again
|
|
policy.AllowedFunctions["test_func"] = true
|
|
|
|
// Create a new context (with sandbox enabled)
|
|
ctx = NewRenderContext(engine.environment, nil, engine)
|
|
ctx.EnableSandbox()
|
|
|
|
// Reset buffer
|
|
buf.Reset()
|
|
|
|
// Rendering should succeed now
|
|
err = template.nodes.Render(&buf, ctx)
|
|
if err != nil {
|
|
t.Errorf("Rendering failed after allowing function: %v", err)
|
|
}
|
|
|
|
expected := "test function called"
|
|
if buf.String() != expected {
|
|
t.Errorf("Expected rendered output '%s', got '%s'", expected, buf.String())
|
|
}
|
|
}
|
|
|
|
// TestSandboxFilters tests if the sandbox can restrict filter access
|
|
func TestSandboxFilters(t *testing.T) {
|
|
// Create a fresh engine
|
|
engine := New()
|
|
|
|
// Create a default security policy that doesn't allow any filters
|
|
policy := NewDefaultSecurityPolicy()
|
|
policy.AllowedFilters = map[string]bool{} // Start with no allowed filters
|
|
|
|
// Register a test filter through a custom extension
|
|
engine.AddExtension(&TestExtension{
|
|
filters: map[string]FilterFunc{
|
|
"test_filter": func(value interface{}, args ...interface{}) (interface{}, error) {
|
|
return "filtered content", nil
|
|
},
|
|
},
|
|
})
|
|
|
|
// Enable sandbox mode with the restrictive policy
|
|
engine.EnableSandbox(policy)
|
|
|
|
// Register a template that uses the filter
|
|
err := engine.RegisterString("sandbox_filter_test", "{{ 'anything'|test_filter }}")
|
|
if err != nil {
|
|
t.Fatalf("Error registering template: %v", err)
|
|
}
|
|
|
|
// Render in sandbox mode (should fail)
|
|
ctx := NewRenderContext(engine.environment, nil, engine)
|
|
ctx.EnableSandbox() // Enable sandbox mode explicitly in context
|
|
|
|
// Try to render
|
|
var buf bytes.Buffer
|
|
template, err := engine.Load("sandbox_filter_test")
|
|
if err != nil {
|
|
t.Fatalf("Error loading template: %v", err)
|
|
}
|
|
|
|
// Rendering should fail because the filter is not allowed
|
|
err = template.nodes.Render(&buf, ctx)
|
|
if err == nil {
|
|
t.Errorf("Expected sandbox to block unauthorized filter, but it didn't")
|
|
} else {
|
|
t.Logf("Correctly got error: %v", err)
|
|
}
|
|
|
|
// Now allow the filter and try again
|
|
policy.AllowedFilters["test_filter"] = true
|
|
|
|
// Create a new context (with sandbox enabled)
|
|
ctx = NewRenderContext(engine.environment, nil, engine)
|
|
ctx.EnableSandbox()
|
|
|
|
// Reset buffer
|
|
buf.Reset()
|
|
|
|
// Rendering should succeed now
|
|
err = template.nodes.Render(&buf, ctx)
|
|
if err != nil {
|
|
t.Errorf("Rendering failed after allowing filter: %v", err)
|
|
}
|
|
|
|
expected := "filtered content"
|
|
if buf.String() != expected {
|
|
t.Errorf("Expected rendered output '%s', got '%s'", expected, buf.String())
|
|
}
|
|
}
|
|
|
|
// TestSandboxOption tests the sandbox flag on render context
|
|
func TestSandboxOption(t *testing.T) {
|
|
// Create a fresh engine
|
|
engine := New()
|
|
|
|
// Create a security policy that allows specific functions
|
|
policy := NewDefaultSecurityPolicy()
|
|
policy.AllowedFunctions = map[string]bool{
|
|
"safe_func": true, // This function is allowed in sandboxed includes
|
|
}
|
|
|
|
// Register both safe and dangerous functions
|
|
engine.AddExtension(&TestExtension{
|
|
functions: map[string]FunctionFunc{
|
|
"safe_func": func(args ...interface{}) (interface{}, error) {
|
|
return "safe function called", nil
|
|
},
|
|
"dangerous_func": func(args ...interface{}) (interface{}, error) {
|
|
return "dangerous function called", nil
|
|
},
|
|
},
|
|
})
|
|
|
|
// Enable sandbox mode with the policy
|
|
engine.EnableSandbox(policy)
|
|
|
|
// Create a standard (non-sandboxed) context
|
|
ctx := NewRenderContext(engine.environment, nil, engine)
|
|
|
|
// Verify the context is not sandboxed initially
|
|
if ctx.IsSandboxed() {
|
|
t.Errorf("Context should not be sandboxed initially")
|
|
}
|
|
|
|
// Create a child context for an include with sandbox option
|
|
// This simulates what happens in IncludeNode.Render
|
|
includeCtx := NewRenderContext(ctx.env, make(map[string]interface{}), ctx.engine)
|
|
|
|
// Explicitly enable sandbox
|
|
includeCtx.EnableSandbox()
|
|
|
|
// Verify the child context is now sandboxed
|
|
if !includeCtx.IsSandboxed() {
|
|
t.Errorf("Child context should be sandboxed after EnableSandbox()")
|
|
}
|
|
|
|
// Verify safe function works in sandbox mode
|
|
evalNode := &FunctionNode{
|
|
name: "safe_func",
|
|
args: []Node{},
|
|
}
|
|
|
|
result, err := includeCtx.EvaluateExpression(evalNode)
|
|
if err != nil {
|
|
t.Errorf("Safe function should work in sandbox mode: %v", err)
|
|
} else {
|
|
if result != "safe function called" {
|
|
t.Errorf("Unexpected result from safe function: got %v, expected 'safe function called'", result)
|
|
}
|
|
}
|
|
|
|
// Verify dangerous function fails in sandbox mode
|
|
evalDangerousNode := &FunctionNode{
|
|
name: "dangerous_func",
|
|
args: []Node{},
|
|
}
|
|
|
|
_, err = includeCtx.EvaluateExpression(evalDangerousNode)
|
|
if err == nil {
|
|
t.Errorf("Dangerous function should be blocked in sandbox mode")
|
|
} else {
|
|
// Verify the error message mentions the dangerous function
|
|
if msg := err.Error(); !strings.Contains(msg, "dangerous_func") {
|
|
t.Errorf("Expected error to mention 'dangerous_func', but got: %v", err)
|
|
} else {
|
|
t.Logf("Correctly got error: %v", err)
|
|
}
|
|
}
|
|
}
|