From 92ac6ac999a4928cfdb92c485a048e4d51f471d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Graber?= 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 Signed-off-by: Stéphane Graber --- .../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