Version in base suite: 6.0.4-2+deb13u4 Base version: incus_6.0.4-2+deb13u4 Target version: incus_6.0.4-2+deb13u5 Base file: /srv/ftp-master.debian.org/ftp/pool/main/i/incus/incus_6.0.4-2+deb13u4.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/i/incus/incus_6.0.4-2+deb13u5.dsc changelog | 12 + patches/109-CVE-2026-28384.patch | 188 +++++++++++++++++++++++++++++ patches/110-CVE-2026-33542.patch | 208 ++++++++++++++++++++++++++++++++ patches/111-CVE-2026-33743.patch | 51 +++++++ patches/112-CVE-2026-33897.patch | 250 +++++++++++++++++++++++++++++++++++++++ patches/series | 4 6 files changed, 712 insertions(+), 1 deletion(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpstzuq6oh/incus_6.0.4-2+deb13u4.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpstzuq6oh/incus_6.0.4-2+deb13u5.dsc: no acceptable signature found diff -Nru incus-6.0.4/debian/changelog incus-6.0.4/debian/changelog --- incus-6.0.4/debian/changelog 2026-01-22 04:06:05.000000000 +0000 +++ incus-6.0.4/debian/changelog 2026-03-24 22:18:57.000000000 +0000 @@ -1,3 +1,13 @@ +incus (6.0.4-2+deb13u5) trixie-security; urgency=high + + * Cherry-pick fixes for the following security issues: + - CVE-2026-28384 / GHSA-4rmf-rcp8-2r9g + - CVE-2026-33542 / GHSA-p8mm-23gg-jc9r + - CVE-2026-33743 / GHSA-vg76-xmhg-j5x3 + - CVE-2026-33897 / GHSA-83xr-5xxr-mh92 + + -- Mathias Gibbens Tue, 24 Mar 2026 22:18:57 +0000 + incus (6.0.4-2+deb13u4) trixie-security; urgency=high * Cherry-pick fixes for the following security issues: @@ -14,7 +24,7 @@ incus (6.0.4-2+deb13u2) trixie-security; urgency=high - * Backport upstream fix for GHSA-56mx-8g9f-5crf + * Backport upstream fix for CVE-2025-64507 / GHSA-56mx-8g9f-5crf -- Mathias Gibbens Mon, 10 Nov 2025 13:32:56 +0000 diff -Nru incus-6.0.4/debian/patches/109-CVE-2026-28384.patch incus-6.0.4/debian/patches/109-CVE-2026-28384.patch --- incus-6.0.4/debian/patches/109-CVE-2026-28384.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/109-CVE-2026-28384.patch 2026-03-24 22:18:57.000000000 +0000 @@ -0,0 +1,188 @@ +From a16b672a03e366e9bc841518135a50b0f1dbccfd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 24 Feb 2026 18:55:22 -0500 +Subject: [PATCH 1/9] shared/validate: Allow a specific set of compressors +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + shared/validate/validate.go | 29 +++++++++++++++++++++++++++-- + 1 file changed, 27 insertions(+), 2 deletions(-) + +diff --git a/shared/validate/validate.go b/shared/validate/validate.go +index bc35fda045a..838b9a8f0e4 100644 +--- a/shared/validate/validate.go ++++ b/shared/validate/validate.go +@@ -620,11 +620,12 @@ func IsPCIAddress(value string) error { + + // IsCompressionAlgorithm validates whether a value is a valid compression algorithm and is available on the system. + func IsCompressionAlgorithm(value string) error { ++ // none doesn't need any checking. + if value == "none" { + return nil + } + +- // Going to look up tar2sqfs executable binary ++ // Going to look up tar2sqfs executable binary. + if value == "squashfs" { + value = "tar2sqfs" + } +@@ -635,8 +636,32 @@ func IsCompressionAlgorithm(value string) error { + return err + } + ++ if len(fields) == 0 { ++ return fmt.Errorf("Invalid compressor provided") ++ } ++ ++ // Check that we're dealing with a supported option. ++ if !slices.Contains([]string{ ++ "bzip2", ++ "gzip", ++ "lzma", ++ "pigz", ++ "pzstd", ++ "pxz", ++ "tar2sqfs", ++ "xz", ++ "zstd", ++ }, fields[0]) { ++ return fmt.Errorf("Compression algorithm %q isn't currently supported", fields[0]) ++ } ++ ++ // Check that the command exists on the system. + _, err = exec.LookPath(fields[0]) +- return err ++ if err != nil { ++ return fmt.Errorf("Compression algorithm %q isn't supported on this system", fields[0]) ++ } ++ ++ return nil + } + + // IsArchitecture validates whether the value is a valid architecture name. + +From cd05e344cc5e42ae525189538991e7e8eac3020b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 24 Feb 2026 19:04:07 -0500 +Subject: [PATCH 2/9] incusd: Validate CompressionAlgorithm everywhere it's + received +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/images.go | 5 +++++ + cmd/incusd/instance_backup.go | 8 ++++++++ + cmd/incusd/storage_volumes_backup.go | 7 +++++++ + 3 files changed, 20 insertions(+) + +diff --git a/cmd/incusd/images.go b/cmd/incusd/images.go +index bde91d570e9..91a89bb9780 100644 +--- a/cmd/incusd/images.go ++++ b/cmd/incusd/images.go +@@ -59,6 +59,7 @@ import ( + "github.com/lxc/incus/v6/shared/logger" + "github.com/lxc/incus/v6/shared/osarch" + "github.com/lxc/incus/v6/shared/util" ++ "github.com/lxc/incus/v6/shared/validate" + ) + + var imagesCmd = APIEndpoint{ +@@ -290,6 +290,11 @@ func imgPostInstanceInfo(ctx context.Context, s *state.State, r *http.Request, r + var writer io.Writer + + if req.CompressionAlgorithm != "" { ++ err := validate.IsCompressionAlgorithm(req.CompressionAlgorithm) ++ if err != nil { ++ return nil, err ++ } ++ + compress = req.CompressionAlgorithm + } else { + var p *api.Project +diff --git a/cmd/incusd/instance_backup.go b/cmd/incusd/instance_backup.go +index caf12270676..a92027e40fe 100644 +--- a/cmd/incusd/instance_backup.go ++++ b/cmd/incusd/instance_backup.go +@@ -28,6 +28,7 @@ import ( + internalUtil "github.com/lxc/incus/v6/internal/util" + "github.com/lxc/incus/v6/internal/version" + "github.com/lxc/incus/v6/shared/api" ++ "github.com/lxc/incus/v6/shared/validate" + ) + + // swagger:operation GET /1.0/instances/{name}/backups instances instance_backups_get +@@ -278,6 +279,13 @@ func instanceBackupsPost(d *Daemon, r *http.Request) response.Response { + return response.BadRequest(err) + } + ++ if req.CompressionAlgorithm != "" { ++ err := validate.IsCompressionAlgorithm(req.CompressionAlgorithm) ++ if err != nil { ++ return response.BadRequest(err) ++ } ++ } ++ + if req.Name == "" { + // come up with a name. + backups, err := inst.Backups() +diff --git a/cmd/incusd/storage_volumes_backup.go b/cmd/incusd/storage_volumes_backup.go +index ec5c9a9a6..93f01cb14 100644 +--- a/cmd/incusd/storage_volumes_backup.go ++++ b/cmd/incusd/storage_volumes_backup.go +@@ -28,6 +28,7 @@ import ( + "github.com/lxc/incus/v6/internal/version" + "github.com/lxc/incus/v6/shared/api" + "github.com/lxc/incus/v6/shared/logger" ++ "github.com/lxc/incus/v6/shared/validate" + ) + + var storagePoolVolumeTypeCustomBackupsCmd = APIEndpoint{ +@@ -386,6 +386,13 @@ func storagePoolVolumeTypeCustomBackupsPost(d *Daemon, r *http.Request) response + return response.BadRequest(err) + } + ++ if req.CompressionAlgorithm != "" { ++ err := validate.IsCompressionAlgorithm(req.CompressionAlgorithm) ++ if err != nil { ++ return response.BadRequest(err) ++ } ++ } ++ + if req.Name == "" { + var backups []string + +From 31e1dbaae8d9b05c2ae6e16f5ee3f3a5b44efa92 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 24 Feb 2026 19:36:40 -0500 +Subject: [PATCH 3/9] tests: Check compression algorithm validation +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + test/suites/basic.sh | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/test/suites/basic.sh b/test/suites/basic.sh +index a550d8065a6..e647fb0d88a 100644 +--- a/test/suites/basic.sh ++++ b/test/suites/basic.sh +@@ -204,6 +204,9 @@ test_basic_usage() { + incus publish bar --alias=foo-image-compressed --compression="gzip --rsyncable" prop=val1 + incus image delete foo-image-compressed + ++ ! incus publish bar --compression="ps" || false ++ ! incus export bar --compression="ps" || false ++ + # Test privileged container publish + incus profile create priv + incus profile set priv security.privileged true diff -Nru incus-6.0.4/debian/patches/110-CVE-2026-33542.patch incus-6.0.4/debian/patches/110-CVE-2026-33542.patch --- incus-6.0.4/debian/patches/110-CVE-2026-33542.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/110-CVE-2026-33542.patch 2026-03-24 22:18:57.000000000 +0000 @@ -0,0 +1,208 @@ +From 97900d0b493828a07967364cc9e5ad4c24c6a5cd Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 14:36:00 -0400 +Subject: [PATCH 1/4] client: Make ImageFileRequest require a ReadWriteSeeker +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This is a small Go API break which is needed to address a security issue +where we need the ability to re-hash the final image files. + +This is part of a fix for CVE-2026-33542. + +Reported-by: wl2018 +Signed-off-by: Stéphane Graber +--- + client/interfaces.go | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/client/interfaces.go b/client/interfaces.go +index 9b3d6bceb8..8b1e9e79a4 100644 +--- a/client/interfaces.go ++++ b/client/interfaces.go +@@ -502,10 +502,10 @@ type ImageCreateArgs struct { + // The ImageFileRequest struct is used for an image download request. + type ImageFileRequest struct { + // Writer for the metadata file +- MetaFile io.WriteSeeker ++ MetaFile io.ReadWriteSeeker + + // Writer for the rootfs file +- RootfsFile io.WriteSeeker ++ RootfsFile io.ReadWriteSeeker + + // Progress handler (called whenever some progress is made) + ProgressHandler func(progress ioprogress.ProgressData) +-- +2.47.3 + +From 168b4e7432a44c5d142c27ac01002c6d704f2996 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 14:42:43 -0400 +Subject: [PATCH 2/4] incus: Update for changes to incus.ImageFileRequest +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This is part of a fix for CVE-2026-33542. + +Reported-by: wl2018 +Signed-off-by: Stéphane Graber +--- + cmd/incus/image.go | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/cmd/incus/image.go b/cmd/incus/image.go +index 0ae7265e6f..4f72801f80 100644 +--- a/cmd/incus/image.go ++++ b/cmd/incus/image.go +@@ -576,8 +576,8 @@ func (c *cmdImageExport) Run(cmd *cobra.Command, args []string) error { + } + + req := incus.ImageFileRequest{ +- MetaFile: io.WriteSeeker(dest), +- RootfsFile: io.WriteSeeker(destRootfs), ++ MetaFile: io.ReadWriteSeeker(dest), ++ RootfsFile: io.ReadWriteSeeker(destRootfs), + ProgressHandler: progress.UpdateProgress, + } + +-- +2.47.3 + +From 2601db4e4710b11aadea01f88b5b14f484a7525f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 14:42:48 -0400 +Subject: [PATCH 3/4] incusd: Update for changes to incus.ImageFileRequest +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This is part of a fix for CVE-2026-33542. + +Reported-by: wl2018 +Signed-off-by: Stéphane Graber +--- + cmd/incusd/daemon_images.go | 4 ++-- + cmd/incusd/images.go | 4 ++-- + 2 files changed, 4 insertions(+), 4 deletions(-) + +diff --git a/cmd/incusd/daemon_images.go b/cmd/incusd/daemon_images.go +index 501523c2a4..1c75b06a90 100644 +--- a/cmd/incusd/daemon_images.go ++++ b/cmd/incusd/daemon_images.go +@@ -412,8 +412,8 @@ func ImageDownload(ctx context.Context, r *http.Request, s *state.State, op *ope + // Download the image + var resp *incus.ImageFileResponse + request := incus.ImageFileRequest{ +- MetaFile: io.WriteSeeker(dest), +- RootfsFile: io.WriteSeeker(destRootfs), ++ MetaFile: io.ReadWriteSeeker(dest), ++ RootfsFile: io.ReadWriteSeeker(destRootfs), + ProgressHandler: progress, + Canceler: canceler, + DeltaSourceRetriever: func(fingerprint string, file string) string { +diff --git a/cmd/incusd/images.go b/cmd/incusd/images.go +index 91a89bb978..f3c320db87 100644 +--- a/cmd/incusd/images.go ++++ b/cmd/incusd/images.go +@@ -4555,8 +4555,8 @@ func imageImportFromNode(imagesDir string, client incus.InstanceServer, fingerpr + defer func() { _ = rootfsFile.Close() }() + + getReq := incus.ImageFileRequest{ +- MetaFile: io.WriteSeeker(metaFile), +- RootfsFile: io.WriteSeeker(rootfsFile), ++ MetaFile: io.ReadWriteSeeker(metaFile), ++ RootfsFile: io.ReadWriteSeeker(rootfsFile), + } + + getResp, err := client.GetImageFile(fingerprint, getReq) +-- +2.47.3 + +From 93a88451fda06672390dfddd9434b460f4fbfe18 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 14:48:12 -0400 +Subject: [PATCH 4/4] client/simplestreams: Validate the full image hash +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Following download of the files/deltas, compute a full hash to make sure +we have the expected image. + +This is part of a fix for CVE-2026-33542. + +Reported-by: wl2018 +Signed-off-by: Stéphane Graber +--- + client/simplestreams_images.go | 46 ++++++++++++++++++++++++++++++++++ + 1 file changed, 46 insertions(+) + +diff --git a/client/simplestreams_images.go b/client/simplestreams_images.go +index 68b9357fc4..a2fdfad2c2 100644 +--- a/client/simplestreams_images.go ++++ b/client/simplestreams_images.go +@@ -97,6 +97,14 @@ func (r *ProtocolSimpleStreams) GetImageFile(fingerprint string, req ImageFileRe + httpTransport.ResponseHeaderTimeout = 30 * time.Second + httpClient.Transport = httpTransport + ++ // Get the image and expand the fingerprint. ++ image, err := r.ssClient.GetImage(fingerprint) ++ if err != nil { ++ return nil, err ++ } ++ ++ fingerprint = image.Fingerprint ++ + // Get the file list + files, err := r.ssClient.GetFiles(fingerprint) + if err != nil { +@@ -240,6 +248,44 @@ func (r *ProtocolSimpleStreams) GetImageFile(fingerprint string, req ImageFileRe + } + } + ++ // Validate the full image hash. ++ // ++ // Normally we'd do that as we download the image to avoid having to ++ // re-read the data, but because the simplestreams allows retries (HTTP to HTTPS), ++ // we don't have a clean reader that can be used for that. ++ // ++ // Another situation where we couldn't do a streaming hash anyway is when processing delta images. ++ hash256 := sha256.New() ++ ++ if resp.MetaSize > 0 && req.MetaFile != nil { ++ _, err = req.MetaFile.Seek(0, io.SeekStart) ++ if err != nil { ++ return nil, err ++ } ++ ++ _, err := io.Copy(hash256, req.MetaFile) ++ if err != nil { ++ return nil, err ++ } ++ } ++ ++ if resp.RootfsSize > 0 && req.RootfsFile != nil { ++ _, err = req.RootfsFile.Seek(0, io.SeekStart) ++ if err != nil { ++ return nil, err ++ } ++ ++ _, err := io.Copy(hash256, req.RootfsFile) ++ if err != nil { ++ return nil, err ++ } ++ } ++ ++ hash := fmt.Sprintf("%x", hash256.Sum(nil)) ++ if hash != fingerprint { ++ return nil, fmt.Errorf("Image fingerprint doesn't match. Got %s expected %s", hash, fingerprint) ++ } ++ + return &resp, nil + } + +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/111-CVE-2026-33743.patch incus-6.0.4/debian/patches/111-CVE-2026-33743.patch --- incus-6.0.4/debian/patches/111-CVE-2026-33743.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/111-CVE-2026-33743.patch 2026-03-24 22:18:57.000000000 +0000 @@ -0,0 +1,51 @@ +From f00e4104ba13e1eeaa97a113105901a21138ba7b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 23 Mar 2026 22:16:01 -0400 +Subject: [PATCH] incusd/storage/s3: Don't assume backup structure +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Properly skip anything that doesn't have the expected path prefix for a +file within the bucket. Then use strings.TrimPrefix rather than a fixed +offset to clear the prefix. + +This addresses CVE-2026-33743 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +--- + internal/server/storage/s3/transfer_manager.go | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/internal/server/storage/s3/transfer_manager.go b/internal/server/storage/s3/transfer_manager.go +index 4be673d168..094b5093b5 100644 +--- a/internal/server/storage/s3/transfer_manager.go ++++ b/internal/server/storage/s3/transfer_manager.go +@@ -7,6 +7,7 @@ import ( + "net/http" + "net/url" + "os" ++ "strings" + "time" + + "github.com/minio/minio-go/v7" +@@ -130,13 +131,12 @@ func (t TransferManager) UploadAllFiles(bucketName string, srcData io.ReadSeeker + break // End of archive. + } + +- // Skip index.yaml file +- if hdr.Name == "backup/index.yaml" { ++ // Skip anything that's not in the bucket itself. ++ if !strings.HasPrefix(hdr.Name, "backup/bucket/") { + continue + } + +- // Skip directories because they are part of the key of an actual file +- fileName := hdr.Name[len("backup/bucket/"):] ++ fileName := strings.TrimPrefix(hdr.Name, "backup/bucket/") + + _, err = minioClient.PutObject(ctx, bucketName, fileName, tr, -1, minio.PutObjectOptions{}) + if err != nil { +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/112-CVE-2026-33897.patch incus-6.0.4/debian/patches/112-CVE-2026-33897.patch --- incus-6.0.4/debian/patches/112-CVE-2026-33897.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/112-CVE-2026-33897.patch 2026-03-24 22:18:57.000000000 +0000 @@ -0,0 +1,250 @@ +From 932f363a68709ea2e129f8df911c0a48b5dcdd80 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 24 Mar 2026 16:10:25 -0400 +Subject: [PATCH] incusd/instance: Use restricted pongo2 parser +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The chroot logic in pongo2 doesn't work and therefore allows all +templates to read and write to arbitrary paths on the host filesystem. + +Given the logic seemingly never worked properly, no template out there +should be dependent on the file related functions being functional. + +Transition to our standard RenderTemplate logic which specifically block +all file related functions. Introduces a new RenderTemplateFile to +handle cases where we want to directly write to a file (useful for +write quotas). + +This addresses CVE-2026-33897 + +Reported-by: https://7asecurity.com +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + .../server/instance/drivers/driver_lxc.go | 16 ++---- + .../server/instance/drivers/driver_qemu.go | 16 ++---- + internal/server/template/chroot.go | 52 ------------------- + internal/util/template.go | 34 +++++++++++- + 4 files changed, 40 insertions(+), 78 deletions(-) + delete mode 100644 internal/server/template/chroot.go + +diff --git a/internal/server/instance/drivers/driver_lxc.go b/internal/server/instance/drivers/driver_lxc.go +index 281330ca36..2498764baf 100644 +--- a/internal/server/instance/drivers/driver_lxc.go ++++ b/internal/server/instance/drivers/driver_lxc.go +@@ -72,7 +72,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/shared/api" +@@ -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() +diff --git a/internal/server/instance/drivers/driver_qemu.go b/internal/server/instance/drivers/driver_qemu.go +index 1c26ca503a..5c98fed767 100644 +--- a/internal/server/instance/drivers/driver_qemu.go ++++ b/internal/server/instance/drivers/driver_qemu.go +@@ -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() +diff --git a/internal/server/template/chroot.go b/internal/server/template/chroot.go +deleted file mode 100644 +index 7ffbd9d555..0000000000 +--- a/internal/server/template/chroot.go ++++ /dev/null +@@ -1,51 +0,0 @@ +-package template +- +-import ( +- "bytes" +- "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, fmt.Errorf("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 +-} +diff --git a/internal/util/template.go b/internal/util/template.go +index 29eaa66756..3e9c1fa433 100644 +--- a/internal/util/template.go ++++ b/internal/util/template.go +@@ -3,11 +3,14 @@ package util + import ( + "errors" + "fmt" ++ "io" + "strings" + + "github.com/flosch/pongo2" + ) + ++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) { +@@ -15,7 +18,7 @@ func RenderTemplate(template string, ctx pongo2.Context) (string, error) { + 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 ++} +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/series incus-6.0.4/debian/patches/series --- incus-6.0.4/debian/patches/series 2026-01-22 04:05:21.000000000 +0000 +++ incus-6.0.4/debian/patches/series 2026-03-24 22:18:57.000000000 +0000 @@ -17,3 +17,7 @@ 106c-GHSA-56mx-8g9f-5crf.patch 107-CVE-2026-23953.patch 108-CVE-2026-23954.patch +109-CVE-2026-28384.patch +110-CVE-2026-33542.patch +111-CVE-2026-33743.patch +112-CVE-2026-33897.patch