VYPR
Critical severityNVD Advisory· Published Mar 26, 2026· Updated Mar 27, 2026

Incus vulnerable to arbitrary file read and write through pongo templates

CVE-2026-33897

Description

Incus is a system container and virtual machine manager. Prior to version 6.23.0, instance template files can be used to cause arbitrary read or writes as root on the host server. Incus allows for pongo2 templates within instances which can be used at various times in the instance lifecycle to template files inside of the instance. This particular implementation of pongo2 within Incus allowed for file read/write but with the expectation that the pongo2 chroot feature would isolate all such access to the instance's filesystem. This was allowed such that a template could theoretically read a file and then generate a new version of said file. Unfortunately the chroot isolation mechanism is entirely skipped by pongo2 leading to easy access to the entire system's filesystem with root privileges. Version 6.23.0 patches the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/lxc/incus/v6Go
< 6.23.06.23.0
github.com/lxc/incusGo
<= 0.7.0

Affected products

1

Patches

1
487edf5984fa

incusd/instance: Use restricted pongo2 parser

https://github.com/lxc/incusStéphane GraberMar 24, 2026via ghsa
4 files changed · +40 78
  • internal/server/instance/drivers/driver_lxc.go+3 13 modified
    @@ -73,7 +73,6 @@ import (
     	"github.com/lxc/incus/v6/internal/server/state"
     	storagePools "github.com/lxc/incus/v6/internal/server/storage"
     	storageDrivers "github.com/lxc/incus/v6/internal/server/storage/drivers"
    -	"github.com/lxc/incus/v6/internal/server/template"
     	localUtil "github.com/lxc/incus/v6/internal/server/util"
     	internalUtil "github.com/lxc/incus/v6/internal/util"
     	"github.com/lxc/incus/v6/internal/version"
    @@ -7454,14 +7453,6 @@ func (d *lxc) templateApplyNow(trigger instance.TemplateTrigger) error {
     				return fmt.Errorf("Failed to read template file: %w", err)
     			}
     
    -			// Restrict filesystem access to within the container's rootfs
    -			tplSet := pongo2.NewSet(fmt.Sprintf("%s-%s", d.name, tpl.Template), template.ChrootLoader{Path: d.RootfsPath()})
    -
    -			tplRender, err := tplSet.FromString("{% autoescape off %}" + string(tplString) + "{% endautoescape %}")
    -			if err != nil {
    -				return fmt.Errorf("Failed to render template: %w", err)
    -			}
    -
     			configGet := func(confKey, confDefault *pongo2.Value) *pongo2.Value {
     				val, ok := d.expandedConfig[confKey.String()]
     				if !ok {
    @@ -7471,8 +7462,7 @@ func (d *lxc) templateApplyNow(trigger instance.TemplateTrigger) error {
     				return pongo2.AsValue(strings.TrimRight(val, "\r\n"))
     			}
     
    -			// Render the template
    -			err = tplRender.ExecuteWriter(pongo2.Context{
    +			err = internalUtil.RenderTemplateFile(w, string(tplString), pongo2.Context{
     				"trigger":    trigger,
     				"path":       tplPath,
     				"container":  containerMeta,
    @@ -7481,9 +7471,9 @@ func (d *lxc) templateApplyNow(trigger instance.TemplateTrigger) error {
     				"devices":    d.expandedDevices,
     				"properties": tpl.Properties,
     				"config_get": configGet,
    -			}, w)
    +			})
     			if err != nil {
    -				return err
    +				return fmt.Errorf("Failed to render template: %w", err)
     			}
     
     			return w.Close()
    
  • internal/server/instance/drivers/driver_qemu.go+4 12 modified
    @@ -75,7 +75,6 @@ import (
     	"github.com/lxc/incus/v6/internal/server/state"
     	storagePools "github.com/lxc/incus/v6/internal/server/storage"
     	storageDrivers "github.com/lxc/incus/v6/internal/server/storage/drivers"
    -	pongoTemplate "github.com/lxc/incus/v6/internal/server/template"
     	localUtil "github.com/lxc/incus/v6/internal/server/util"
     	localvsock "github.com/lxc/incus/v6/internal/server/vsock"
     	internalUtil "github.com/lxc/incus/v6/internal/util"
    @@ -3604,13 +3603,6 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e
     				return fmt.Errorf("Failed to read template file: %w", err)
     			}
     
    -			// Restrict filesystem access to within the instance's rootfs.
    -			tplSet := pongo2.NewSet(fmt.Sprintf("%s-%s", d.name, tpl.Template), pongoTemplate.ChrootLoader{Path: d.TemplatesPath()})
    -			tplRender, err := tplSet.FromString("{% autoescape off %}" + string(tplString) + "{% endautoescape %}")
    -			if err != nil {
    -				return fmt.Errorf("Failed to render template: %w", err)
    -			}
    -
     			configGet := func(confKey, confDefault *pongo2.Value) *pongo2.Value {
     				val, ok := d.expandedConfig[confKey.String()]
     				if !ok {
    @@ -3621,18 +3613,18 @@ func (d *qemu) templateApplyNow(trigger instance.TemplateTrigger, path string) e
     			}
     
     			// Render the template.
    -			err = tplRender.ExecuteWriter(pongo2.Context{
    +			err = internalUtil.RenderTemplateFile(w, string(tplString), pongo2.Context{
     				"trigger":    trigger,
     				"path":       tplPath,
    -				"instance":   instanceMeta,
     				"container":  instanceMeta, // FIXME: remove once most images have moved away.
    +				"instance":   instanceMeta,
     				"config":     d.expandedConfig,
     				"devices":    d.expandedDevices,
     				"properties": tpl.Properties,
     				"config_get": configGet,
    -			}, w)
    +			})
     			if err != nil {
    -				return err
    +				return fmt.Errorf("Failed to render template: %w", err)
     			}
     
     			return w.Close()
    
  • internal/server/template/chroot.go+0 52 removed
    @@ -1,52 +0,0 @@
    -package template
    -
    -import (
    -	"bytes"
    -	"errors"
    -	"fmt"
    -	"io"
    -	"os"
    -	"path/filepath"
    -	"strings"
    -)
    -
    -// ChrootLoader is a pong2 compatible file loader which restricts all accesses to a directory.
    -type ChrootLoader struct {
    -	Path string
    -}
    -
    -// Abs resolves a filename relative to the base directory. Absolute paths are allowed.
    -// When there's no base dir set, the absolute path to the filename
    -// will be calculated based on either the provided base directory (which
    -// might be a path of a template which includes another template) or
    -// the current working directory.
    -func (l ChrootLoader) Abs(base string, name string) string {
    -	return filepath.Clean(fmt.Sprintf("%s/%s", l.Path, name))
    -}
    -
    -// Get reads the path's content from your local filesystem.
    -func (l ChrootLoader) Get(path string) (io.Reader, error) {
    -	// Get the full path
    -	path, err := filepath.EvalSymlinks(path)
    -	if err != nil {
    -		return nil, err
    -	}
    -
    -	basePath, err := filepath.EvalSymlinks(l.Path)
    -	if err != nil {
    -		return nil, err
    -	}
    -
    -	// Validate that we're under the expected prefix
    -	if !strings.HasPrefix(path, basePath) {
    -		return nil, errors.New("Attempting to access a file outside the instance")
    -	}
    -
    -	// Open and read the file
    -	buf, err := os.ReadFile(path)
    -	if err != nil {
    -		return nil, err
    -	}
    -
    -	return bytes.NewReader(buf), nil
    -}
    
  • internal/util/template.go+33 1 modified
    @@ -3,19 +3,22 @@ package util
     import (
     	"errors"
     	"fmt"
    +	"io"
     	"strings"
     
     	"github.com/flosch/pongo2/v6"
     )
     
    +var bannedTemplateTags = []string{"extends", "import", "include", "ssi"}
    +
     // RenderTemplate renders a pongo2 template with nesting support.
     // This supports up to 3 levels of nesting (to avoid loops).
     func RenderTemplate(template string, ctx pongo2.Context) (string, error) {
     	// Prepare a custom set.
     	custom := pongo2.NewSet("render-template", pongo2.DefaultLoader)
     
     	// Block the use of some tags.
    -	for _, tag := range []string{"extends", "import", "include", "ssi"} {
    +	for _, tag := range bannedTemplateTags {
     		err := custom.BanTag(tag)
     		if err != nil {
     			return "", fmt.Errorf("Failed to configure custom pongo2 parser: Failed to block tag tag %q: %w", tag, err)
    @@ -47,3 +50,32 @@ func RenderTemplate(template string, ctx pongo2.Context) (string, error) {
     
     	return "", errors.New("Maximum template recursion limit reached")
     }
    +
    +// RenderTemplateFile renders a pongo2 template to a file.
    +// No nesting is supported in this scenario.
    +func RenderTemplateFile(w io.Writer, template string, ctx pongo2.Context) error {
    +	// Prepare a custom set.
    +	custom := pongo2.NewSet("render-template", pongo2.DefaultLoader)
    +
    +	// Block the use of some tags.
    +	for _, tag := range bannedTemplateTags {
    +		err := custom.BanTag(tag)
    +		if err != nil {
    +			return fmt.Errorf("Failed to configure custom pongo2 parser: Failed to block tag tag %q: %w", tag, err)
    +		}
    +	}
    +
    +	// Load template from string
    +	tpl, err := custom.FromString("{% autoescape off %}" + template + "{% endautoescape %}")
    +	if err != nil {
    +		return err
    +	}
    +
    +	// Get rendered template
    +	err = tpl.ExecuteWriter(ctx, w)
    +	if err != nil {
    +		return err
    +	}
    +
    +	return nil
    +}
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.