From 92ac6ac999a4928cfdb92c485a048e4d51f471d0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?St=C3=A9phane=20Graber?= <stgraber@stgraber.org>
Date: Wed, 21 Jan 2026 00:04:37 -0500
Subject: [PATCH] incusd/instance/lxc: Restrict path of template files and
 targets
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This fixes three security issues related to file templates:

 - The template target path could be made to be relative or gothrough
   symlinks in a way that could lead to arbitrary write to the host
   filesystem.

 - The template directory could be relative, allowing for arbitrary read
   from the host filesystem.

 - The template file itself could be made relative, allowing for
   arbitrary reads from the host filesystem.

In the case of the template target path, the new logic makes use of the
kernel's openat2 system call which brings a variety of flags that can be
used to restrict path resolution and detect potential issues.

For the template path itself, we now validate that it is a simple local
file and that the template directory isn't a symlink.

This fixes CVE-2026-23954

Reported-by: Rory McNamara <rory.mcnamara@snyk.io>
Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
---
 .../server/instance/drivers/driver_lxc.go     | 58 ++++++++++++++++++-
 1 file changed, 57 insertions(+), 1 deletion(-)

diff --git a/internal/server/instance/drivers/driver_lxc.go b/internal/server/instance/drivers/driver_lxc.go
index b6d8cb9a0a7..a1e4f6bbe0d 100644
--- a/internal/server/instance/drivers/driver_lxc.go
+++ b/internal/server/instance/drivers/driver_lxc.go
@@ -6841,6 +6841,32 @@ func (d *lxc) templateApplyNow(trigger instance.TemplateTrigger) error {
 		containerMeta["privileged"] = "false"
 	}
 
+	// Setup security check.
+	rootfsPath, err := os.OpenFile(d.RootfsPath(), unix.O_PATH, 0)
+	if err != nil {
+		return fmt.Errorf("Failed to open instance rootfs path: %w", err)
+	}
+
+	defer func() { _ = rootfsPath.Close() }()
+
+	checkBeneath := func(targetPath string) error {
+		fd, err := unix.Openat2(int(rootfsPath.Fd()), targetPath, &unix.OpenHow{
+			Flags:   unix.O_PATH | unix.O_CLOEXEC,
+			Resolve: unix.RESOLVE_BENEATH | unix.RESOLVE_NO_MAGICLINKS,
+		})
+		if err != nil {
+			if errors.Is(err, unix.EXDEV) {
+				return errors.New("Template is attempting access to path outside of container")
+			}
+
+			return nil
+		}
+
+		_ = unix.Close(fd)
+
+		return nil
+	}
+
 	// Go through the templates
 	for tplPath, tpl := range metadata.Templates {
 		err = func(tplPath string, tpl *api.ImageMetadataTemplate) error {
@@ -6853,8 +6879,38 @@ func (d *lxc) templateApplyNow(trigger instance.TemplateTrigger) error {
 				return nil
 			}
 
+			// Perform some security checks.
+			relPath := strings.TrimLeft(tplPath, "/")
+
+			err = checkBeneath(relPath)
+			if err != nil {
+				return err
+			}
+
+			if filepath.Base(tpl.Template) != tpl.Template {
+				return errors.New("Template path is attempting to read outside of template directory")
+			}
+
+			tplDirStat, err := os.Lstat(d.TemplatesPath())
+			if err != nil {
+				return fmt.Errorf("Couldn't access template directory: %w", err)
+			}
+
+			if !tplDirStat.IsDir() {
+				return errors.New("Template directory isn't a regular directory")
+			}
+
+			tplFileStat, err := os.Lstat(filepath.Join(d.TemplatesPath(), tpl.Template))
+			if err != nil {
+				return fmt.Errorf("Couldn't access template file: %w", err)
+			}
+
+			if tplFileStat.Mode()&os.ModeSymlink == os.ModeSymlink {
+				return errors.New("Template file is a symlink")
+			}
+
 			// Open the file to template, create if needed
-			fullpath := filepath.Join(d.RootfsPath(), strings.TrimLeft(tplPath, "/"))
+			fullpath := filepath.Join(d.RootfsPath(), relPath)
 			if util.PathExists(fullpath) {
 				if tpl.CreateOnly {
 					return nil
