Skip to content

Latest commit

 

History

History
481 lines (353 loc) · 13 KB

File metadata and controls

481 lines (353 loc) · 13 KB

Security and Sandboxing

pongo2 provides several security features to protect against template injection attacks, restrict template capabilities, and isolate template sets from each other.

DefaultSet Security Warning

Important: The package-level convenience functions (pongo2.FromFile(), pongo2.FromString(), etc.) use a DefaultSet that is configured with a LocalFilesystemLoader. This loader has unrestricted access to the local filesystem.

For production applications, especially those that:

  • Handle user-generated template content
  • Need to restrict template capabilities
  • Require sandboxing

Create a custom template set with appropriate loaders and restrictions instead of using the DefaultSet.

// Instead of using pongo2.FromFile() (which uses DefaultSet), do this:

// Option 1: Use embedded templates (most secure)
//go:embed templates/*
var templateFS embed.FS
loader := pongo2.NewFSLoader(templateFS)
set := pongo2.NewSet("secure", loader)

// Option 2: Use LocalFilesystemLoader with sandbox restrictions
loader := pongo2.MustNewLocalFileSystemLoader("/var/templates")
set := pongo2.NewSet("restricted", loader)
set.BanTag("include")   // Prevent file inclusion
set.BanTag("import")    // Prevent macro imports
set.BanTag("ssi")       // Prevent server-side includes
set.BanTag("extends")   // Prevent template inheritance
set.BanFilter("safe")   // Prevent autoescape bypass

// Now use the custom set
tpl, err := set.FromFile("page.html")

See Template Sets for more information on creating and configuring template sets.

Automatic HTML Escaping

By default, pongo2 automatically escapes HTML special characters in variable output to prevent XSS (Cross-Site Scripting) attacks.

How Autoescape Works

{{ user_input }}

If user_input contains <script>alert('xss')</script>, the output will be:

&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;

Configuring Autoescape

Global setting (DefaultSet):

// Disable autoescape globally
pongo2.SetAutoescape(false)

// Enable autoescape globally (default)
pongo2.SetAutoescape(true)

Per-set setting:

set := pongo2.NewSet("my-set", loader)
set.SetAutoescape(false)  // Disable for this set only

Per-template block:

{% autoescape off %}
  {{ trusted_html }}  {# Will NOT be escaped #}
{% endautoescape %}

{% autoescape on %}
  {{ user_input }}  {# Will be escaped #}
{% endautoescape %}

Per-variable:

{{ trusted_html|safe }}  {# Mark as safe, skip escaping #}

Best Practices for Escaping

  1. Keep autoescape enabled (default) for user-facing templates
  2. Use |safe sparingly and only for content you fully control
  3. Use |escapejs for JavaScript contexts:
    <script>
    var data = "{{ user_input|escapejs }}";
    </script>
  4. Validate and sanitize user input before it reaches templates

Sandbox Features

Template sets support sandboxing to restrict what templates can do.

Banning Tags

Prevent templates from using specific tags:

set := pongo2.NewSet("sandboxed", loader)

// Ban potentially dangerous tags
set.BanTag("include")   // Prevent file inclusion
set.BanTag("import")    // Prevent macro imports from other files
set.BanTag("ssi")       // Prevent server-side includes
set.BanTag("extends")   // Prevent template inheritance

// Now load templates - they can't use banned tags
tpl, err := set.FromFile("user-template.html")

Banning Filters

Prevent templates from using specific filters:

set := pongo2.NewSet("restricted", loader)

// Ban filters that bypass security
set.BanFilter("safe")      // Prevent bypassing autoescape

// Ban filters based on your security requirements
set.BanFilter("escapejs")  // If you don't want JS output

Important Timing Restriction

Tags and filters must be banned BEFORE the first template is loaded:

set := pongo2.NewSet("test", loader)

// This works - no templates loaded yet
set.BanTag("ssi")

// Load a template
tpl, _ := set.FromFile("page.html")

// This FAILS - too late!
err := set.BanTag("include")
// err: "you cannot ban any tags after you've added your first template..."

This restriction exists because:

  1. Bans are checked at parse time for efficiency
  2. Once a template is parsed, it's cached
  3. Allowing late bans would be confusing (some templates might have used the tag already)

Sandbox Example: User-Generated Templates

For user-submitted templates (e.g., email templates, CMS content):

func createSandboxedSet(loader pongo2.TemplateLoader) *pongo2.TemplateSet {
    set := pongo2.NewSet("user-content", loader)

    // Prevent file system access
    set.BanTag("include")
    set.BanTag("import")
    set.BanTag("ssi")
    set.BanTag("extends")

    // Prevent autoescape bypass
    set.BanFilter("safe")

    // Limit to basic control flow only
    // (all other tags remain available: if, for, with, set, etc.)

    return set
}

Template Set Isolation

Each TemplateSet is fully isolated with its own:

  • Tag registry - Custom tags are per-set
  • Filter registry - Custom filters are per-set
  • Globals - Global variables are per-set
  • Cache - Template cache is per-set
  • Banned tags/filters - Sandbox restrictions are per-set
  • Options - TrimBlocks, LStripBlocks are per-set
  • Autoescape setting - Can be configured per-set

Per-Set Custom Extensions

Register tags and filters specific to a template set:

// Create isolated sets
webSet := pongo2.NewSet("web", webLoader)
emailSet := pongo2.NewSet("email", emailLoader)

// Register filters only for web templates
webSet.RegisterFilter("asset_url", assetUrlFilter)

// Register tags only for email templates
emailSet.RegisterTag("unsubscribe_link", unsubscribeLinkParser)

// These filters/tags are NOT available in the other set

Per-Set Globals

webSet.Globals["site_url"] = "https://example.com"
webSet.Globals["current_year"] = time.Now().Year()

emailSet.Globals["company_name"] = "ACME Corp"
emailSet.Globals["support_email"] = "support@example.com"

Template Loader Security

Understanding LocalFileSystemLoader

Important: The LocalFileSystemLoader's base directory is NOT a security feature. It only serves as the root path for resolving relative template paths to absolute paths. Templates can still access files outside the base directory using absolute paths.

// The base directory is for path resolution, NOT security
loader := pongo2.MustNewLocalFileSystemLoader("/var/templates")
set := pongo2.NewSet("app", loader)

// Relative paths are resolved from the base directory
tpl, _ := set.FromFile("pages/home.html")  // Loads /var/templates/pages/home.html

// WARNING: Absolute paths bypass the base directory entirely
tpl, _ := set.FromFile("/etc/passwd")  // This would work if not restricted!

For actual security, use sandbox features to restrict file inclusion:

set := pongo2.NewSet("sandboxed", loader)
set.BanTag("include")   // Prevent {% include %}
set.BanTag("import")    // Prevent {% import %}
set.BanTag("ssi")       // Prevent {% ssi %}
set.BanTag("extends")   // Prevent {% extends %}

Custom Loaders for Real Security

Implement a custom loader with additional restrictions:

type SecureLoader struct {
    baseDir    string
    allowedExt []string
}

func (l *SecureLoader) Abs(base, name string) string {
    // Resolve path relative to base directory
    resolved := filepath.Join(filepath.Dir(base), name)

    // Ensure result is within allowed directory
    if !strings.HasPrefix(resolved, l.baseDir) {
        return "" // Return empty to indicate not found
    }

    return resolved
}

func (l *SecureLoader) Get(path string) (io.Reader, error) {
    // Check file extension
    ext := filepath.Ext(path)
    allowed := false
    for _, e := range l.allowedExt {
        if ext == e {
            allowed = true
            break
        }
    }
    if !allowed {
        return nil, fmt.Errorf("file extension not allowed: %s", ext)
    }

    return os.Open(path)
}

Macro Recursion Protection

pongo2 limits macro recursion to prevent stack overflow:

{% macro infinite() %}
  {{ infinite() }}  {# This will eventually fail #}
{% endmacro %}

The maximum recursion depth is 1000 calls. When exceeded:

maximum recursive macro call depth reached (max is 1000)

This protects against:

  • Accidental infinite recursion in user templates
  • Denial of service via deeply nested macro calls

Context Security

Public vs Private Context

type ExecutionContext struct {
    Public  Context  // User-provided data (read-only by convention)
    Private Context  // Internal engine data (forloop, macro args, etc.)
    Shared  Context  // Shared across included templates
}
  • Public: Data you provide via tpl.Execute(ctx). Templates can read but shouldn't modify.
  • Private: Internal variables like forloop. Templates can access but not override user data.
  • Shared: Persists across {% include %} calls.

Context Identifier Validation

Context keys must be valid identifiers:

// Valid
ctx := pongo2.Context{
    "user":     user,
    "item_1":   item,
}

// Invalid - will panic
ctx := pongo2.Context{
    "'invalid": value,  // Can't start with quote
    "foo-bar":  value,  // Can't contain hyphen
    "foo.bar":  value,  // Can't contain dot
}

Error Handling and Information Leakage

Production Error Handling

Avoid exposing template internals to users:

func handler(w http.ResponseWriter, r *http.Request) {
    tpl, err := templates.FromCache("page.html")
    if err != nil {
        // Log detailed error internally
        log.Printf("Template error: %v", err)

        // Return generic error to user
        http.Error(w, "Internal Server Error", 500)
        return
    }

    err = tpl.ExecuteWriter(ctx, w)
    if err != nil {
        // Log detailed error internally
        log.Printf("Execution error: %v", err)

        // Don't expose template paths or variable names
        http.Error(w, "Error rendering page", 500)
    }
}

Debug Mode

Set Debug = false in production:

set := pongo2.NewSet("production", loader)
set.Debug = false  // Disable debug logging, enable caching

// In development
devSet := pongo2.NewSet("development", loader)
devSet.Debug = true  // Enable debug logging, disable caching

Security Checklist

For User-Facing Templates

  • Keep autoescape enabled (default)
  • Use |escapejs for JavaScript contexts
  • Validate user input before passing to templates
  • Use parameterized queries, not template string concatenation for SQL
  • Review all uses of |safe filter

For User-Generated Templates

  • Create a dedicated sandboxed TemplateSet
  • Ban include, import, ssi, extends tags
  • Ban safe filter
  • Use a restricted template loader
  • Set resource limits (template size, execution time) at the application level
  • Consider banning complex expressions if not needed

For Production Deployment

  • Set Debug = false
  • Don't expose detailed template errors to users
  • Use FromCache for performance and to prevent repeated parsing
  • Monitor template execution times
  • Use separate template sets for different trust levels

Common Vulnerabilities and Mitigations

Server-Side Template Injection (SSTI)

Risk: If user input is directly concatenated into template strings:

// DANGEROUS - NEVER DO THIS
template := "Hello " + userInput + "!"
tpl, _ := pongo2.FromString(template)

Mitigation: Always pass user input via context:

// SAFE
tpl, _ := pongo2.FromString("Hello {{ name }}!")
tpl.Execute(pongo2.Context{"name": userInput})

Cross-Site Scripting (XSS)

Risk: Rendering unsanitized user input.

Mitigation:

  1. Keep autoescape enabled
  2. Use |escapejs for JavaScript
  3. Validate/sanitize input before it reaches templates

Path Traversal

Risk: User-controlled template names could access unintended files.

Mitigation:

  1. Never pass user input directly to FromFile - the LocalFileSystemLoader base directory does NOT prevent path traversal (absolute paths bypass it entirely)
  2. Ban file inclusion tags if templates come from untrusted sources:
    set.BanTag("include")
    set.BanTag("import")
    set.BanTag("extends")
    set.BanTag("ssi")
  3. Use an allowlist of permitted template names in your application code:
    allowedTemplates := map[string]bool{
        "home.html": true,
        "about.html": true,
    }
    if !allowedTemplates[templateName] {
        return errors.New("template not allowed")
    }
  4. Implement a secure custom loader that validates and restricts paths (see "Custom Loaders for Real Security" above)

Denial of Service

Risk: Complex templates, deep recursion, or large contexts could exhaust resources.

Mitigation:

  1. Macro recursion is automatically limited to 1000 calls
  2. Implement application-level timeouts for template execution
  3. Limit context data size
  4. Use template caching (FromCache)