Version in base suite: 6.0.4-2+deb13u10 Version in overlay suite: 6.0.4-2+deb13u11 Base version: incus_6.0.4-2+deb13u11 Target version: incus_6.0.4-2+deb13u12 Base file: /srv/ftp-master.debian.org/ftp/pool/main/i/incus/incus_6.0.4-2+deb13u11.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/i/incus/incus_6.0.4-2+deb13u12.dsc changelog | 36 + patches/156-cherry-pick-skip-processing-limits-vm.patch | 36 + patches/157a-cherry-pick-strip-unsafe-symlinks.patch | 189 ++++++++++ patches/157b-cherry-pick-allow-expected-symlinks.patch | 41 ++ patches/157c-GHSA-5pgx-q9xx-j3qv.patch | 300 ++++++++++++++++ patches/158-GHSA-jcw9-v2ff-8m79.patch | 125 ++++++ patches/159a-cherry-pick-CheckTrustStateIssuer.patch | 70 +++ patches/159b-GHSA-xg75-xf37-cph9.patch | 221 +++++++++++ patches/160-GHSA-p45f-gqqj-6qq3.patch | 57 +++ patches/161-GHSA-cwg9-8mc4-hf7j.patch | 106 +++++ patches/162-GHSA-5c64-j5m6-8433.patch | 152 ++++++++ patches/163-GHSA-9c46-v3hw-v6m8.patch | 43 ++ patches/164-GHSA-h2j8-4crh-vhfc.patch | 107 +++++ patches/165-GHSA-8r3h-vc36-6xpx.patch | 138 +++++++ patches/166-GHSA-x6pw-mh5w-pq73.patch | 33 + patches/167-GHSA-pf5w-m8m6-5265.patch | 73 +++ patches/168-GHSA-f7xp-22ph-v444.patch | 38 ++ patches/169-GHSA-9f75-5fjw-5jvv.patch | 52 ++ patches/series | 17 19 files changed, 1827 insertions(+), 7 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpzn_fdogo/incus_6.0.4-2+deb13u11.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpzn_fdogo/incus_6.0.4-2+deb13u12.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-09-25 14:31:18.000000000 +0000 +++ incus-6.0.4/debian/changelog 2026-10-29 22:31:36.000000000 +0000 @@ -1,15 +1,37 @@ +incus (6.0.4-2+deb13u12) trixie-security; urgency=high + + * Cherry-pick fixes for the following security issues: + - GHSA-5pgx-q9xx-j3qv + - GHSA-jcw9-v2ff-8m79 + - GHSA-xg75-xf37-cph9 + - GHSA-p45f-gqqj-6qq3 + - GHSA-cwg9-8mc4-hf7j + - GHSA-5c64-j5m6-8433 + - GHSA-9c46-v3hw-v6m8 + - GHSA-h2j8-4crh-vhfc + - GHSA-8r3h-vc36-6xpx + - GHSA-x6pw-mh5w-pq73 + - GHSA-pf5w-m8m6-5265 + - GHSA-f7xp-22ph-v444 + - GHSA-9f75-5fjw-5jvv + * Cherry-pick upstream fix to properly enforce + restricted.virtual-machines.lowlevel when creating a VM (thanks to + Whitelify for identifying the missing fix in trixie) + + -- Mathias Gibbens Thu, 29 Oct 2026 22:31:36 +0000 + incus (6.0.4-2+deb13u11) trixie-security; urgency=high * Cherry-pick fix for only transferring xattrs settable without CAP_SYS_ADMIN * Cherry-pick fixes for the following security issues: - - GHSA-4cph-ccqv-hm3c - - GHSA-579w-c4rw-c8q3 - - GHSA-x8gj-2q73-qr6j - - GHSA-jh4v-j34r-2mgh - - GHSA-mfwv-x733-9446 - - GHSA-h85r-gjgx-g2rv - - GHSA-27q7-qwhm-c34p + - CVE-2026-85526 / GHSA-h85r-gjgx-g2rv + - CVE-2026-85185 / GHSA-27q7-qwhm-c34p + - CVE-2026-115372 / GHSA-4cph-ccqv-hm3c + - CVE-2026-115377 / GHSA-579w-c4rw-c8q3 + - CVE-2026-115371 / GHSA-x8gj-2q73-qr6j + - CVE-2026-115376 / GHSA-jh4v-j34r-2mgh + - CVE-2026-115373 / GHSA-mfwv-x733-9446 -- Mathias Gibbens Fri, 25 Sep 2026 14:31:18 +0000 diff -Nru incus-6.0.4/debian/patches/156-cherry-pick-skip-processing-limits-vm.patch incus-6.0.4/debian/patches/156-cherry-pick-skip-processing-limits-vm.patch --- incus-6.0.4/debian/patches/156-cherry-pick-skip-processing-limits-vm.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/156-cherry-pick-skip-processing-limits-vm.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,36 @@ +From 3cee83b1d397ecf7019673e1a9b6b1b0fb20f981 Mon Sep 17 00:00:00 2001 +From: Piotr Resztak +Date: Tue, 22 Jul 2025 10:39:50 +0200 +Subject: [PATCH] incusd/project: Skip processing 'limits.processes' for VM + instance types + +Signed-off-by: Piotr Resztak +--- + internal/server/project/permissions.go | 7 +++++++ + 1 file changed, 7 insertions(+) + +diff --git a/internal/server/project/permissions.go b/internal/server/project/permissions.go +index 1ebc54cbc42..48e7fb6f6b4 100644 +--- a/internal/server/project/permissions.go ++++ b/internal/server/project/permissions.go +@@ -90,6 +90,7 @@ func AllowInstanceCreation(tx *db.ClusterTx, projectName string, req api.Instanc + Name: req.Name, + Project: projectName, + InstancePut: req.InstancePut, ++ Type: string(req.Type), + }) + + // Special case restriction checks on volatile.* keys. +@@ -1463,6 +1464,12 @@ func getInstanceLimits(inst api.Instance, keys []string, skipUnset bool) (map[st + limit += sizeStateLimit + } + } else { ++ // Skip processing for 'limits.processes' if the instance type is VM, ++ // as this limit is only applicable to containers. ++ if key == "limits.processes" && inst.Type == instancetype.VM.String() { ++ continue ++ } ++ + value, ok := inst.Config[key] + if !ok || value == "" { + if skipUnset { diff -Nru incus-6.0.4/debian/patches/157a-cherry-pick-strip-unsafe-symlinks.patch incus-6.0.4/debian/patches/157a-cherry-pick-strip-unsafe-symlinks.patch --- incus-6.0.4/debian/patches/157a-cherry-pick-strip-unsafe-symlinks.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/157a-cherry-pick-strip-unsafe-symlinks.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,189 @@ +From 62d944bf9f30730d4a0bb5485d4265ebcd3d0adb Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 00:04:23 -0400 +Subject: [PATCH] incusd/storage: Strip unsafe symlinks in externally-supplied + instance data +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +There is no reason for there to be any symlink within the instance metadata. + +Given the number of issues we've run into due to symlinks in recent +months, let's add some extra checks/cleanup that will just strip any +symlink in sight outside of the rootfs. + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/storage/backend.go | 108 ++++++++++++++++++++++++++++- + 1 file changed, 107 insertions(+), 1 deletion(-) + +diff --git a/internal/server/storage/backend.go b/internal/server/storage/backend.go +index 041310abf8..53ec220656 100644 +--- a/internal/server/storage/backend.go ++++ b/internal/server/storage/backend.go +@@ -736,6 +736,69 @@ func (b *backend) CreateInstance(inst instance.Instance, op *operations.Operatio + return nil + } + ++// stripMetadataLinks removes any symlink in the instance's metadata area (everything but the guest ++// rootfs) and returns the removed paths. Removing rather than just reporting neutralizes links that ++// reach the volume via a path (e.g. snapshot restore) that cannot be cleanly reverted. ++func stripMetadataLinks(instPath string) ([]string, error) { ++ rootfsPath := filepath.Join(instPath, "rootfs") ++ ++ var removed []string ++ ++ err := filepath.WalkDir(instPath, func(path string, d fs.DirEntry, err error) error { ++ if err != nil { ++ return err ++ } ++ ++ // Don't descend into the guest's own root filesystem. ++ if path == rootfsPath && d.IsDir() { ++ return filepath.SkipDir ++ } ++ ++ if d.Type()&fs.ModeSymlink != 0 { ++ err = os.Remove(path) ++ if err != nil { ++ return err ++ } ++ ++ removed = append(removed, path) ++ } ++ ++ return nil ++ }) ++ if err != nil { ++ return nil, err ++ } ++ ++ return removed, nil ++} ++ ++// stripInstanceMetadataLinks mounts the instance, strips disallowed metadata symlinks and errors if any were found. ++func (b *backend) stripInstanceMetadataLinks(inst instance.Instance, op *operations.Operation) error { ++ volType, err := InstanceTypeToVolumeType(inst.Type()) ++ if err != nil { ++ return err ++ } ++ ++ _, err = b.MountInstance(inst, op) ++ if err != nil { ++ return err ++ } ++ ++ defer func() { _ = b.UnmountInstance(inst, op) }() ++ ++ removed, err := stripMetadataLinks(drivers.GetVolumeMountPath(b.name, volType, project.Instance(inst.Project().Name, inst.Name()))) ++ if err != nil { ++ return err ++ } ++ ++ if len(removed) > 0 { ++ b.logger.Warn("Removed disallowed symlinks from instance metadata", logger.Ctx{"instance": inst.Name(), "project": inst.Project().Name, "paths": removed}) ++ return fmt.Errorf("Instance metadata contained disallowed symlinks: %v", removed) ++ } ++ ++ return nil ++} ++ + // CreateInstanceFromBackup restores a backup file onto the storage device. Because the backup file + // is unpacked and restored onto the storage device before the instance is created in the database + // it is necessary to return two functions; a post hook that can be run once the instance has been +@@ -799,6 +862,16 @@ func (b *backend) CreateInstanceFromBackup(srcBackup backup.Info, srcData io.Rea + importRevert.Add(revertHook) + } + ++ // Strip unsafe symlinks before any host-side use of the metadata area. ++ removed, err := stripMetadataLinks(vol.MountPath()) ++ if err != nil { ++ return nil, nil, err ++ } ++ ++ if len(removed) > 0 { ++ return nil, nil, fmt.Errorf("Backup metadata contained disallowed symlinks: %v", removed) ++ } ++ + err = b.ensureInstanceSymlink(instanceType, srcBackup.Project, srcBackup.Name, vol.MountPath()) + if err != nil { + return nil, nil, err +@@ -1241,6 +1314,12 @@ func (b *backend) CreateInstanceFromCopy(inst instance.Instance, src instance.In + } + } + ++ // Strip unsafe symlinks, including any hidden in a copied-from snapshot. ++ err = b.stripInstanceMetadataLinks(inst, op) ++ if err != nil { ++ return err ++ } ++ + revert.Success() + return nil + } +@@ -1741,6 +1820,12 @@ func (b *backend) RefreshInstance(inst instance.Instance, src instance.Instance, + return err + } + ++ // Strip unsafe symlinks materialized from the refresh source. ++ err = b.stripInstanceMetadataLinks(inst, op) ++ if err != nil { ++ return err ++ } ++ + revert.Success() + return nil + } +@@ -1916,6 +2001,12 @@ func (b *backend) CreateInstanceFromImage(inst instance.Instance, fingerprint st + return err + } + ++ // Strip unsafe symlinks introduced by the image. ++ err = b.stripInstanceMetadataLinks(inst, op) ++ if err != nil { ++ return err ++ } ++ + err = inst.DeferTemplateApply(instance.TemplateTriggerCreate) + if err != nil { + return err +@@ -2181,6 +2272,14 @@ func (b *backend) CreateInstanceFromMigration(inst instance.Instance, conn io.Re + } + } + ++ // Strip unsafe symlinks from migrated data, skipping intra-cluster moves (no new data, may not be locally mountable). ++ if !isRemoteClusterMove { ++ err = b.stripInstanceMetadataLinks(inst, op) ++ if err != nil { ++ return err ++ } ++ } ++ + revert.Success() + return nil + } +@@ -3214,7 +3313,7 @@ func (b *backend) DeleteInstanceSnapshot(inst instance.Instance, op *operations. + } + + // RestoreInstanceSnapshot restores an instance snapshot. +-func (b *backend) RestoreInstanceSnapshot(inst instance.Instance, src instance.Instance, op *operations.Operation) error { ++func (b *backend) RestoreInstanceSnapshot(inst instance.Instance, src instance.Instance, op *operations.Operation) (err error) { + l := b.logger.AddContext(logger.Ctx{"project": inst.Project().Name, "instance": inst.Name(), "src": src.Name()}) + l.Debug("RestoreInstanceSnapshot started") + defer l.Debug("RestoreInstanceSnapshot finished") +@@ -3222,6 +3321,13 @@ func (b *backend) RestoreInstanceSnapshot(inst instance.Instance, src instance.I + revert := revert.New() + defer revert.Fail() + ++ // Strip unsafe symlinks from restored content on all success paths (a snapshot may hide them and restore can't be reverted). ++ defer func() { ++ if err == nil { ++ err = b.stripInstanceMetadataLinks(inst, op) ++ } ++ }() ++ + if inst.Type() != src.Type() { + return fmt.Errorf("Instance types must match") + } diff -Nru incus-6.0.4/debian/patches/157b-cherry-pick-allow-expected-symlinks.patch incus-6.0.4/debian/patches/157b-cherry-pick-allow-expected-symlinks.patch --- incus-6.0.4/debian/patches/157b-cherry-pick-allow-expected-symlinks.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/157b-cherry-pick-allow-expected-symlinks.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,41 @@ +From 1d5bd3846240e60c244c072cc7dec4e9ea9be2ae Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 24 Jul 2026 21:59:18 -0400 +Subject: [PATCH] incusd/storage: Allow expected symlinks in instance metadata +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Stéphane Graber +--- + internal/server/storage/backend.go | 17 +++++++++++++++++ + 1 file changed, 17 insertions(+) + +diff --git a/internal/server/storage/backend.go b/internal/server/storage/backend.go +index 9fb2e2cfb2e..7d23bd9eed2 100644 +--- a/internal/server/storage/backend.go ++++ b/internal/server/storage/backend.go +@@ -747,6 +747,23 @@ func stripMetadataLinks(instPath string) ([]string, error) { + } + + if d.Type()&fs.ModeSymlink != 0 { ++ relPath, err := filepath.Rel(instPath, path) ++ if err != nil { ++ return err ++ } ++ ++ // Allow the links Incus itself maintains. ++ if relPath == "qemu.nvram" || relPath == "config/lxd-agent" { ++ target, err := os.Readlink(path) ++ if err != nil { ++ return err ++ } ++ ++ if !strings.Contains(target, "/") && target != "." && target != ".." { ++ return nil ++ } ++ } ++ + err = os.Remove(path) + if err != nil { + return err diff -Nru incus-6.0.4/debian/patches/157c-GHSA-5pgx-q9xx-j3qv.patch incus-6.0.4/debian/patches/157c-GHSA-5pgx-q9xx-j3qv.patch --- incus-6.0.4/debian/patches/157c-GHSA-5pgx-q9xx-j3qv.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/157c-GHSA-5pgx-q9xx-j3qv.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,300 @@ +From 695ff59c6ec893d77f3357c114e6aab551eab34a Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Thu, 22 Oct 2026 08:18:11 -0400 +Subject: [PATCH] incusd/storage: Strip non-regular files from instance + metadata +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses GHSA-5pgx-q9xx-j3qv (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/backup/backup_config_utils.go | 8 ++- + .../server/instance/drivers/driver_common.go | 30 +++++++++ + .../server/instance/drivers/driver_lxc.go | 4 +- + .../server/instance/drivers/driver_qemu.go | 4 +- + internal/server/storage/backend.go | 62 ++++++++++++------- + 5 files changed, 78 insertions(+), 30 deletions(-) + +diff --git a/internal/server/backup/backup_config_utils.go b/internal/server/backup/backup_config_utils.go +index 04d82b2a01..269d9fd9fd 100644 +--- a/internal/server/backup/backup_config_utils.go ++++ b/internal/server/backup/backup_config_utils.go +@@ -2,7 +2,9 @@ package backup + + import ( + "context" ++ "errors" + "fmt" ++ "io/fs" + "os" + "path/filepath" + +@@ -173,9 +175,13 @@ func UpdateInstanceConfig(c *db.Cluster, b Info, mountPath string) error { + return fmt.Errorf("No root device could be found") + } + +- // Write updated backup.yaml file. ++ // Delete then recreate with O_EXCL so we never write through a planted hard link or symlink. ++ err = os.Remove(backupFilePath) ++ if err != nil && !errors.Is(err, fs.ErrNotExist) { ++ return err ++ } + +- file, err := os.Create(backupFilePath) ++ file, err := os.OpenFile(backupFilePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o400) + if err != nil { + return err + } +diff --git a/internal/server/instance/drivers/driver_common.go b/internal/server/instance/drivers/driver_common.go +index 393d6c0a12..e3b28d73bb 100644 +--- a/internal/server/instance/drivers/driver_common.go ++++ b/internal/server/instance/drivers/driver_common.go +@@ -5,6 +5,7 @@ import ( + "context" + "errors" + "fmt" ++ "io" + "math" + "net/http" + "os" +@@ -487,6 +488,35 @@ func (d *common) TemplatesPath() string { + return filepath.Join(d.Path(), "templates") + } + ++// readRegularFileInRoot reads name from within dir, refusing to follow symlinks out of it. ++// Snapshots may carry symlinks from imported data, so only a regular file is accepted. ++func readRegularFileInRoot(dir string, name string) ([]byte, error) { ++ root, err := os.OpenRoot(dir) ++ if err != nil { ++ return nil, err ++ } ++ ++ defer func() { _ = root.Close() }() ++ ++ fi, err := root.Lstat(name) ++ if err != nil { ++ return nil, err ++ } ++ ++ if !fi.Mode().IsRegular() { ++ return nil, fmt.Errorf("%q isn't a regular file", name) ++ } ++ ++ f, err := root.Open(name) ++ if err != nil { ++ return nil, err ++ } ++ ++ defer func() { _ = f.Close() }() ++ ++ return io.ReadAll(f) ++} ++ + // StoragePool returns the storage pool name. + func (d *common) StoragePool() (string, error) { + pool, err := d.getStoragePool() +diff --git a/internal/server/instance/drivers/driver_lxc.go b/internal/server/instance/drivers/driver_lxc.go +index 2a8633d085..643a098885 100644 +--- a/internal/server/instance/drivers/driver_lxc.go ++++ b/internal/server/instance/drivers/driver_lxc.go +@@ -5102,8 +5102,8 @@ func (d *lxc) Export(w io.Writer, properties map[string]string, expiration time. + fnam := filepath.Join(cDir, "metadata.yaml") + + if util.PathExists(fnam) { +- // Parse the metadata. +- content, err := os.ReadFile(fnam) ++ // Parse the metadata, confined to the instance directory so an imported snapshot's symlink isn't followed. ++ content, err := readRegularFileInRoot(cDir, "metadata.yaml") + if err != nil { + _ = tarWriter.Close() + d.logger.Error("Failed exporting instance", ctxMap) +diff --git a/internal/server/instance/drivers/driver_qemu.go b/internal/server/instance/drivers/driver_qemu.go +index 90c112c0ec..718dbe85c6 100644 +--- a/internal/server/instance/drivers/driver_qemu.go ++++ b/internal/server/instance/drivers/driver_qemu.go +@@ -6502,8 +6502,8 @@ func (d *qemu) Export(w io.Writer, properties map[string]string, expiration time + fnam := filepath.Join(cDir, "metadata.yaml") + + if util.PathExists(fnam) { +- // Parse the metadata. +- content, err := os.ReadFile(fnam) ++ // Parse the metadata, confined to the instance directory so an imported snapshot's symlink isn't followed. ++ content, err := readRegularFileInRoot(cDir, "metadata.yaml") + if err != nil { + _ = tarWriter.Close() + d.logger.Error("Failed exporting instance", ctxMap) +diff --git a/internal/server/storage/backend.go b/internal/server/storage/backend.go +index 352d988ba3..b4ca5f003d 100644 +--- a/internal/server/storage/backend.go ++++ b/internal/server/storage/backend.go +@@ -736,10 +736,11 @@ func (b *backend) CreateInstance(inst instance.Instance, op *operations.Operatio + return nil + } + +-// stripMetadataLinks removes any symlink in the instance's metadata area (everything but the guest +-// rootfs) and returns the removed paths. Removing rather than just reporting neutralizes links that +-// reach the volume via a path (e.g. snapshot restore) that cannot be cleanly reverted. +-func stripMetadataLinks(instPath string) ([]string, error) { ++// stripMetadata removes anything in the instance's metadata area (everything but the guest rootfs) ++// that isn't a directory, a regular file or an allowed self-symlink, and returns the removed paths. ++// Removing rather than just reporting neutralizes entries that reach the volume via a path ++// (e.g. snapshot restore) that cannot be cleanly reverted. ++func stripMetadata(instPath string) ([]string, error) { + rootfsPath := filepath.Join(instPath, "rootfs") + + var removed []string +@@ -754,13 +755,18 @@ func stripMetadataLinks(instPath string) ([]string, error) { + return filepath.SkipDir + } + ++ // Keep directories and regular files. ++ if d.IsDir() || d.Type().IsRegular() { ++ return nil ++ } ++ ++ // Keep the self-symlinks Incus itself maintains. + if d.Type()&fs.ModeSymlink != 0 { + relPath, err := filepath.Rel(instPath, path) + if err != nil { + return err + } + +- // Allow the links Incus itself maintains. + if relPath == "qemu.nvram" || relPath == "config/lxd-agent" { + target, err := os.Readlink(path) + if err != nil { +@@ -771,15 +777,16 @@ func stripMetadataLinks(instPath string) ([]string, error) { + return nil + } + } ++ } + +- err = os.Remove(path) +- if err != nil { +- return err +- } +- +- removed = append(removed, path) ++ // Remove everything else (disallowed symlinks, device nodes, pipes, sockets). ++ err = os.Remove(path) ++ if err != nil { ++ return err + } + ++ removed = append(removed, path) ++ + return nil + }) + if err != nil { +@@ -789,8 +796,8 @@ func stripMetadataLinks(instPath string) ([]string, error) { + return removed, nil + } + +-// stripInstanceMetadataLinks mounts the instance, strips disallowed metadata symlinks and errors if any were found. +-func (b *backend) stripInstanceMetadataLinks(inst instance.Instance, op *operations.Operation) error { ++// stripInstanceMetadata mounts the instance, strips disallowed metadata entries and errors if any were found. ++func (b *backend) stripInstanceMetadata(inst instance.Instance, op *operations.Operation) error { + volType, err := InstanceTypeToVolumeType(inst.Type()) + if err != nil { + return err +@@ -803,14 +810,14 @@ func (b *backend) stripInstanceMetadataLinks(inst instance.Instance, op *operati + + defer func() { _ = b.UnmountInstance(inst, op) }() + +- removed, err := stripMetadataLinks(drivers.GetVolumeMountPath(b.name, volType, project.Instance(inst.Project().Name, inst.Name()))) ++ removed, err := stripMetadata(drivers.GetVolumeMountPath(b.name, volType, project.Instance(inst.Project().Name, inst.Name()))) + if err != nil { + return err + } + + if len(removed) > 0 { +- b.logger.Warn("Removed disallowed symlinks from instance metadata", logger.Ctx{"instance": inst.Name(), "project": inst.Project().Name, "paths": removed}) +- return fmt.Errorf("Instance metadata contained disallowed symlinks: %v", removed) ++ b.logger.Warn("Removed disallowed entries from instance metadata", logger.Ctx{"instance": inst.Name(), "project": inst.Project().Name, "paths": removed}) ++ return fmt.Errorf("Instance metadata contained disallowed entries: %v", removed) + } + + return nil +@@ -880,13 +887,13 @@ func (b *backend) CreateInstanceFromBackup(srcBackup backup.Info, srcData io.Rea + } + + // Strip unsafe symlinks before any host-side use of the metadata area. +- removed, err := stripMetadataLinks(vol.MountPath()) ++ removed, err := stripMetadata(vol.MountPath()) + if err != nil { + return nil, nil, err + } + + if len(removed) > 0 { +- return nil, nil, fmt.Errorf("Backup metadata contained disallowed symlinks: %v", removed) ++ return nil, nil, fmt.Errorf("Backup metadata contained disallowed entries: %v", removed) + } + + err = b.ensureInstanceSymlink(instanceType, srcBackup.Project, srcBackup.Name, vol.MountPath()) +@@ -1332,7 +1339,7 @@ func (b *backend) CreateInstanceFromCopy(inst instance.Instance, src instance.In + } + + // Strip unsafe symlinks, including any hidden in a copied-from snapshot. +- err = b.stripInstanceMetadataLinks(inst, op) ++ err = b.stripInstanceMetadata(inst, op) + if err != nil { + return err + } +@@ -1838,7 +1845,7 @@ func (b *backend) RefreshInstance(inst instance.Instance, src instance.Instance, + } + + // Strip unsafe symlinks materialized from the refresh source. +- err = b.stripInstanceMetadataLinks(inst, op) ++ err = b.stripInstanceMetadata(inst, op) + if err != nil { + return err + } +@@ -2019,7 +2026,7 @@ func (b *backend) CreateInstanceFromImage(inst instance.Instance, fingerprint st + } + + // Strip unsafe symlinks introduced by the image. +- err = b.stripInstanceMetadataLinks(inst, op) ++ err = b.stripInstanceMetadata(inst, op) + if err != nil { + return err + } +@@ -2291,7 +2298,7 @@ func (b *backend) CreateInstanceFromMigration(inst instance.Instance, conn io.Re + + // Strip unsafe symlinks from migrated data, skipping intra-cluster moves (no new data, may not be locally mountable). + if !isRemoteClusterMove { +- err = b.stripInstanceMetadataLinks(inst, op) ++ err = b.stripInstanceMetadata(inst, op) + if err != nil { + return err + } +@@ -3341,7 +3348,7 @@ func (b *backend) RestoreInstanceSnapshot(inst instance.Instance, src instance.I + // Strip unsafe symlinks from restored content on all success paths (a snapshot may hide them and restore can't be reverted). + defer func() { + if err == nil { +- err = b.stripInstanceMetadataLinks(inst, op) ++ err = b.stripInstanceMetadata(inst, op) + } + }() + +@@ -6565,7 +6572,14 @@ func (b *backend) UpdateInstanceBackupFile(inst instance.Instance, snapshots boo + + // Write the YAML + path := filepath.Join(inst.Path(), "backup.yaml") +- f, err := root.OpenFile("backup.yaml", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o400) ++ ++ // Remove any existing entry first so we never write through a planted hard link. ++ err = root.Remove("backup.yaml") ++ if err != nil && !errors.Is(err, fs.ErrNotExist) { ++ return err ++ } ++ ++ f, err := root.OpenFile("backup.yaml", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o400) + if err != nil { + return fmt.Errorf("Failed to create file %q: %w", path, err) + } +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/158-GHSA-jcw9-v2ff-8m79.patch incus-6.0.4/debian/patches/158-GHSA-jcw9-v2ff-8m79.patch --- incus-6.0.4/debian/patches/158-GHSA-jcw9-v2ff-8m79.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/158-GHSA-jcw9-v2ff-8m79.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,125 @@ +From 9faee327517ca2df581b65754d7cc86ba3ee47ed Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 20 Oct 2026 15:09:45 -0400 +Subject: [PATCH] incusd: Authorize effective project on resource creation +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses GHSA-jcw9-v2ff-8m79 (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/network_acls.go | 6 ++++++ + cmd/incusd/network_zones.go | 6 ++++++ + cmd/incusd/networks.go | 6 ++++++ + cmd/incusd/profiles.go | 6 ++++++ + cmd/incusd/storage_buckets.go | 14 ++++++++++++++ + cmd/incusd/storage_volumes.go | 6 ++++++ + 7 files changed, 62 insertions(+) + +diff --git a/cmd/incusd/network_acls.go b/cmd/incusd/network_acls.go +index 76b098e8c1..69c515a392 100644 +--- a/cmd/incusd/network_acls.go ++++ b/cmd/incusd/network_acls.go +@@ -300,6 +300,12 @@ func networkACLsPost(d *Daemon, r *http.Request) response.Response { + return response.SmartError(err) + } + ++ // Authorize against the project the ACL is actually created in. ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(projectName), auth.EntitlementCanCreateNetworkACLs) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + req := api.NetworkACLsPost{} + + // Parse the request into a record. +diff --git a/cmd/incusd/network_zones.go b/cmd/incusd/network_zones.go +index 4ddfa9145a..31de927a9f 100644 +--- a/cmd/incusd/network_zones.go ++++ b/cmd/incusd/network_zones.go +@@ -290,6 +290,12 @@ func networkZonesPost(d *Daemon, r *http.Request) response.Response { + return response.SmartError(err) + } + ++ // Authorize against the project the zone is actually created in. ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(projectName), auth.EntitlementCanCreateNetworkZones) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + req := api.NetworkZonesPost{} + + // Parse the request into a record. +diff --git a/cmd/incusd/networks.go b/cmd/incusd/networks.go +index 7407639e31..ece7906ac1 100644 +--- a/cmd/incusd/networks.go ++++ b/cmd/incusd/networks.go +@@ -350,6 +350,12 @@ func networksPost(d *Daemon, r *http.Request) response.Response { + return response.SmartError(err) + } + ++ // Authorize against the project the network is actually created in. ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(projectName), auth.EntitlementCanCreateNetworks) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + networkCreateLock.Lock() + defer networkCreateLock.Unlock() + +diff --git a/cmd/incusd/profiles.go b/cmd/incusd/profiles.go +index bd178ce1d1..793253a167 100644 +--- a/cmd/incusd/profiles.go ++++ b/cmd/incusd/profiles.go +@@ -334,6 +334,12 @@ func profilesPost(d *Daemon, r *http.Request) response.Response { + return response.SmartError(err) + } + ++ // Authorize against the project the profile is actually created in. ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(p.Name), auth.EntitlementCanCreateProfiles) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + req := api.ProfilesPost{} + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { +diff --git a/cmd/incusd/storage_buckets.go b/cmd/incusd/storage_buckets.go +index 1259d43d69..10d8729561 100644 +--- a/cmd/incusd/storage_buckets.go ++++ b/cmd/incusd/storage_buckets.go +@@ -436,6 +436,12 @@ func storagePoolBucketsPost(d *Daemon, r *http.Request) response.Response { + return response.SmartError(err) + } + ++ // Authorize against the project the bucket is actually created in. ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(bucketProjectName), auth.EntitlementCanCreateStorageBuckets) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + poolName, err := url.PathUnescape(mux.Vars(r)["poolName"]) + if err != nil { + return response.SmartError(err) +diff --git a/cmd/incusd/storage_volumes.go b/cmd/incusd/storage_volumes.go +index 54c2e5254b..2a385a142b 100644 +--- a/cmd/incusd/storage_volumes.go ++++ b/cmd/incusd/storage_volumes.go +@@ -636,6 +636,12 @@ func storagePoolVolumesPost(d *Daemon, r *http.Request) response.Response { + return response.SmartError(err) + } + ++ // Authorize against the project the volume is actually created in. ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(projectName), auth.EntitlementCanCreateStorageVolumes) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + resp := forwardedResponseIfTargetIsRemote(s, r) + if resp != nil { + return resp +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/159a-cherry-pick-CheckTrustStateIssuer.patch incus-6.0.4/debian/patches/159a-cherry-pick-CheckTrustStateIssuer.patch --- incus-6.0.4/debian/patches/159a-cherry-pick-CheckTrustStateIssuer.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/159a-cherry-pick-CheckTrustStateIssuer.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,70 @@ +From 58c24037009a61fa2cac178d60858132db218f85 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 13 Oct 2026 17:27:52 -0400 +Subject: [PATCH] incusd/util: Add CheckTrustStateIssuer +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Signed-off-by: Stéphane Graber +--- + internal/server/util/http.go | 46 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 46 insertions(+) + +diff --git a/internal/server/util/http.go b/internal/server/util/http.go +index 8e3fbc67d17..2b2813d787a 100644 +--- a/internal/server/util/http.go ++++ b/internal/server/util/http.go +@@ -199,6 +199,52 @@ func CheckTrustState(cert x509.Certificate, trustedCerts map[string]x509.Certifi + return false, "" + } + ++// CheckTrustStateIssuer checks whether the first certificate in the chain was issued by a CA certificate from ++// the trust store and returns the fingerprint of that trust store entry. ++func CheckTrustStateIssuer(chain []*x509.Certificate, trustedCerts map[string]x509.Certificate, networkCert *localtls.CertInfo) (bool, string) { ++ if len(chain) == 0 { ++ return false, "" ++ } ++ ++ // Never trust a certificate that the revocation list rejects. ++ if networkCert != nil { ++ ca := networkCert.CA() ++ crl := networkCert.CRL() ++ if ca != nil && crl != nil && chain[0].CheckSignatureFrom(ca) == nil && crl.CheckSignatureFrom(ca) == nil { ++ for _, revoked := range crl.RevokedCertificateEntries { ++ if chain[0].SerialNumber.Cmp(revoked.SerialNumber) == 0 { ++ return false, "" ++ } ++ } ++ } ++ } ++ ++ intermediates := x509.NewCertPool() ++ for _, cert := range chain[1:] { ++ intermediates.AddCert(cert) ++ } ++ ++ for fingerprint, v := range trustedCerts { ++ // Only CA certificates can vouch for other certificates. ++ if !v.BasicConstraintsValid || !v.IsCA { ++ continue ++ } ++ ++ roots := x509.NewCertPool() ++ roots.AddCert(&v) ++ ++ _, err := chain[0].Verify(x509.VerifyOptions{Roots: roots, Intermediates: intermediates, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}}) ++ if err != nil { ++ continue ++ } ++ ++ logger.Debug("Matched trusted CA cert", logger.Ctx{"fingerprint": fingerprint, "subject": chain[0].Subject}) ++ return true, fingerprint ++ } ++ ++ return false, "" ++} ++ + // IsRecursionRequest checks whether the given HTTP request is marked with the + // "recursion" flag in its form values. + func IsRecursionRequest(r *http.Request) bool { diff -Nru incus-6.0.4/debian/patches/159b-GHSA-xg75-xf37-cph9.patch incus-6.0.4/debian/patches/159b-GHSA-xg75-xf37-cph9.patch --- incus-6.0.4/debian/patches/159b-GHSA-xg75-xf37-cph9.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/159b-GHSA-xg75-xf37-cph9.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,221 @@ +From 6cada9f69ff79af01a584ff6d34599d29982bb29 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 13 Oct 2026 18:42:36 -0400 +Subject: [PATCH] incusd: Only trust the key-proven client certificate +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The HTTPS listener uses tls.RequestClientCert, so the TLS handshake only +proves possession of the first certificate in the client's chain. The +authentication and certificate ownership checks looped over the whole +chain and matched each certificate against the trust store, letting a +client authenticate as, or take over, any trust entry by appending that +entry's public certificate to its own chain. + +Match only the first peer certificate in every trust-store check. + +This addresses GHSA-xg75-xf37-cph9 (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incus-agent/server.go | 10 +++--- + cmd/incusd/api_vsock.go | 4 +-- + cmd/incusd/certificates.go | 18 +++------- + cmd/incusd/daemon.go | 33 +++++++++--------- + internal/server/cluster/tls.go | 39 +++++++++++---------- + 6 files changed, 76 insertions(+), 71 deletions(-) + +diff --git a/cmd/incus-agent/server.go b/cmd/incus-agent/server.go +index 7ee14e3331..44fd7ce392 100644 +--- a/cmd/incus-agent/server.go ++++ b/cmd/incus-agent/server.go +@@ -111,12 +111,10 @@ func createCmd(restAPI *mux.Router, version string, c APIEndpoint, cert *x509.Ce + func authenticate(r *http.Request, cert *x509.Certificate) bool { + clientCerts := map[string]x509.Certificate{"0": *cert} + +- for _, cert := range r.TLS.PeerCertificates { +- trusted, _ := localUtil.CheckTrustState(*cert, clientCerts, nil, false) +- if trusted { +- return true +- } ++ if len(r.TLS.PeerCertificates) == 0 { ++ return false + } + +- return false ++ trusted, _ := localUtil.CheckTrustState(*r.TLS.PeerCertificates[0], clientCerts, nil, false) ++ return trusted + } +diff --git a/cmd/incusd/api_vsock.go b/cmd/incusd/api_vsock.go +index 110d6a1090..9fcdb365e0 100644 +--- a/cmd/incusd/api_vsock.go ++++ b/cmd/incusd/api_vsock.go +@@ -45,8 +45,8 @@ func authenticateAgentCert(s *state.State, r *http.Request) (bool, instance.Inst + + agentCert := inst.(instance.VM).AgentCertificate() + +- for _, cert := range r.TLS.PeerCertificates { +- trusted, _ = localUtil.CheckTrustState(*cert, map[string]x509.Certificate{"0": *agentCert}, nil, false) ++ if len(r.TLS.PeerCertificates) > 0 { ++ trusted, _ = localUtil.CheckTrustState(*r.TLS.PeerCertificates[0], map[string]x509.Certificate{"0": *agentCert}, nil, false) + if trusted { + return true, inst, nil + } +diff --git a/cmd/incusd/certificates.go b/cmd/incusd/certificates.go +index 354835fe84..6ece897b39 100644 +--- a/cmd/incusd/certificates.go ++++ b/cmd/incusd/certificates.go +@@ -724,7 +724,7 @@ func certificatesPost(d *Daemon, r *http.Request) response.Response { + return response.BadRequest(fmt.Errorf("No client certificate provided")) + } + +- cert = r.TLS.PeerCertificates[len(r.TLS.PeerCertificates)-1] ++ cert = r.TLS.PeerCertificates[0] + } else { + return response.BadRequest(fmt.Errorf("Can't use TLS data on non-TLS link")) + } +@@ -1088,12 +1088,8 @@ func doCertificateUpdate(d *Daemon, dbInfo api.Certificate, req api.CertificateP + } + + trusted := false +- for _, i := range r.TLS.PeerCertificates { +- trusted, _ = localUtil.CheckTrustState(*i, trustedCerts, s.Endpoints.NetworkCert(), false) +- +- if trusted { +- break +- } ++ if len(r.TLS.PeerCertificates) > 0 { ++ trusted, _ = localUtil.CheckTrustState(*r.TLS.PeerCertificates[0], trustedCerts, s.Endpoints.NetworkCert(), false) + } + + if !trusted { +@@ -1221,12 +1217,8 @@ func certificateDelete(d *Daemon, r *http.Request) response.Response { + } + + trusted := false +- for _, i := range r.TLS.PeerCertificates { +- trusted, _ = localUtil.CheckTrustState(*i, trustedCerts, s.Endpoints.NetworkCert(), false) +- +- if trusted { +- break +- } ++ if len(r.TLS.PeerCertificates) > 0 { ++ trusted, _ = localUtil.CheckTrustState(*r.TLS.PeerCertificates[0], trustedCerts, s.Endpoints.NetworkCert(), false) + } + + if !trusted { +diff --git a/cmd/incusd/daemon.go b/cmd/incusd/daemon.go +index d481ca161e..4c595c7cf6 100644 +--- a/cmd/incusd/daemon.go ++++ b/cmd/incusd/daemon.go +@@ -488,13 +488,18 @@ func (d *Daemon) Authenticate(w http.ResponseWriter, r *http.Request) (bool, str + return false, "", "", err + } + ++ // The TLS listener only proves possession of the first peer certificate's key, so the rest of ++ // the presented chain is attacker-controlled and must never be matched against the trust store. ++ var peerCert *x509.Certificate ++ if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 { ++ peerCert = r.TLS.PeerCertificates[0] ++ } ++ + // Allow internal cluster traffic by checking against the trusted certfificates. +- if r.TLS != nil { +- for _, i := range r.TLS.PeerCertificates { +- trusted, fingerprint := localUtil.CheckTrustState(*i, trustedCerts[certificate.TypeServer], d.endpoints.NetworkCert(), false) +- if trusted { +- return true, fingerprint, "cluster", nil +- } ++ if peerCert != nil { ++ trusted, fingerprint := localUtil.CheckTrustState(*peerCert, trustedCerts[certificate.TypeServer], d.endpoints.NetworkCert(), false) ++ if trusted { ++ return true, fingerprint, "cluster", nil + } + } + +@@ -560,18 +565,21 @@ func (d *Daemon) Authenticate(w http.ResponseWriter, r *http.Request) (bool, str + } + + // Validate metrics TLS certificates. +- if r.URL.Path == "/1.0/metrics" { +- for _, i := range r.TLS.PeerCertificates { +- trusted, username := localUtil.CheckTrustState(*i, trustedCerts[certificate.TypeMetrics], d.endpoints.NetworkCert(), trustCACertificates) +- if trusted { +- return true, username, api.AuthenticationMethodTLS, nil +- } ++ if r.URL.Path == "/1.0/metrics" && peerCert != nil { ++ trusted, username := localUtil.CheckTrustState(*peerCert, trustedCerts[certificate.TypeMetrics], d.endpoints.NetworkCert(), trustCACertificates) ++ if trusted { ++ return true, username, api.AuthenticationMethodTLS, nil ++ } ++ ++ trusted, username = localUtil.CheckTrustStateIssuer(r.TLS.PeerCertificates, trustedCerts[certificate.TypeMetrics], d.endpoints.NetworkCert()) ++ if trusted { ++ return true, username, api.AuthenticationMethodTLS, nil + } + } + + // Validate regular TLS certificates. +- for _, i := range r.TLS.PeerCertificates { +- trusted, username := localUtil.CheckTrustState(*i, trustedCerts[certificate.TypeClient], d.endpoints.NetworkCert(), trustCACertificates) ++ if peerCert != nil { ++ trusted, username := localUtil.CheckTrustState(*peerCert, trustedCerts[certificate.TypeClient], d.endpoints.NetworkCert(), trustCACertificates) + if trusted { + return true, username, api.AuthenticationMethodTLS, nil + } +diff --git a/internal/server/cluster/tls.go b/internal/server/cluster/tls.go +index 2e31752b8f..a58aa39276 100644 +--- a/internal/server/cluster/tls.go ++++ b/internal/server/cluster/tls.go +@@ -66,25 +66,30 @@ func tlsCheckCert(r *http.Request, networkCert *localtls.CertInfo, serverCert *l + return false + } + +- for _, i := range r.TLS.PeerCertificates { +- // Trust our own server certificate. This allows Dqlite to start with a connection back to this +- // member before the database is available. It also allows us to switch the server certificate to +- // the network certificate during cluster upgrade to per-server certificates, and it be trusted. +- trustedServerCert, _ := x509.ParseCertificate(serverCert.KeyPair().Certificate[0]) +- trusted, _ := localUtil.CheckTrustState(*i, map[string]x509.Certificate{serverCert.Fingerprint(): *trustedServerCert}, networkCert, false) +- if trusted { +- return true +- } +- +- // Check the trusted server certificates list provided. +- trusted, _ = localUtil.CheckTrustState(*i, trustedCerts[certificate.TypeServer], networkCert, false) +- if trusted { +- return true +- } +- +- logger.Errorf("Invalid client certificate %v (%v) from %v", i.Subject, localtls.CertFingerprint(i), r.RemoteAddr) ++ if len(r.TLS.PeerCertificates) == 0 { ++ return false ++ } ++ ++ // Only the first peer certificate is proven by the TLS handshake; ignore the rest of the chain. ++ peerCert := r.TLS.PeerCertificates[0] ++ ++ // Trust our own server certificate. This allows Cowsql to start with a connection back to this ++ // member before the database is available. It also allows us to switch the server certificate to ++ // the network certificate during cluster upgrade to per-server certificates, and it be trusted. ++ trustedServerCert, _ := x509.ParseCertificate(serverCert.KeyPair().Certificate[0]) ++ trusted, _ := localUtil.CheckTrustState(*peerCert, map[string]x509.Certificate{serverCert.Fingerprint(): *trustedServerCert}, networkCert, false) ++ if trusted { ++ return true + } + ++ // Check the trusted server certificates list provided. ++ trusted, _ = localUtil.CheckTrustState(*peerCert, trustedCerts[certificate.TypeServer], networkCert, false) ++ if trusted { ++ return true ++ } ++ ++ logger.Errorf("Invalid client certificate %v (%v) from %v", peerCert.Subject, localtls.CertFingerprint(peerCert), r.RemoteAddr) ++ + return false + } + +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/160-GHSA-p45f-gqqj-6qq3.patch incus-6.0.4/debian/patches/160-GHSA-p45f-gqqj-6qq3.patch --- incus-6.0.4/debian/patches/160-GHSA-p45f-gqqj-6qq3.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/160-GHSA-p45f-gqqj-6qq3.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,57 @@ +From f33e2e6407672c779ebbe661b8ca96a13f3a0c91 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 6 Oct 2026 05:18:00 -0400 +Subject: [PATCH] incusd/storage: Validate snapshot names from the backup index + on import +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The snapshot names listed at the top of the backup index are validated +on import, but the database records are created from the names in the +volume config section of the same index, which weren't. A crafted +backup could therefore store a snapshot name containing path traversal +which is later resolved by the storage driver, for example when the +volume is deleted. + +Validate those names too and require them to match the top level list. + +This addresses GHSA-p45f-gqqj-6qq3 (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/storage/backend.go | 10 ++++++++++ + 1 file changed, 10 insertions(+) + +diff --git a/internal/server/storage/backend.go b/internal/server/storage/backend.go +index 42b91956b8..6a44c07bf2 100644 +--- a/internal/server/storage/backend.go ++++ b/internal/server/storage/backend.go +@@ -57,6 +57,7 @@ import ( + "github.com/lxc/incus/v6/shared/revert" + "github.com/lxc/incus/v6/shared/units" + "github.com/lxc/incus/v6/shared/util" ++ "github.com/lxc/incus/v6/shared/validate" + ) + + var ( +@@ -7262,6 +7263,16 @@ func (b *backend) CreateCustomVolumeFromBackup(srcBackup backup.Info, srcData io + _, snapName, _ = api.GetParentAndSnapshotName(snapshot.Name) + } + ++ // The name is used as a path segment and must match the backup content. ++ err = validate.IsAPIName(snapName, false) ++ if err != nil { ++ return fmt.Errorf("Invalid snapshot name: %w", err) ++ } ++ ++ if !slices.Contains(srcBackup.Snapshots, snapName) { ++ return fmt.Errorf("Snapshot %q in index doesn't match backup content", snapName) ++ } ++ + fullSnapName := drivers.GetSnapshotVolumeName(srcBackup.Name, snapName) + snapVolStorageName := project.StorageVolume(srcBackup.Project, fullSnapName) + snapVol := b.GetVolume(drivers.VolumeTypeCustom, drivers.ContentType(srcBackup.Config.Volume.ContentType), snapVolStorageName, snapshot.Config) +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/161-GHSA-cwg9-8mc4-hf7j.patch incus-6.0.4/debian/patches/161-GHSA-cwg9-8mc4-hf7j.patch --- incus-6.0.4/debian/patches/161-GHSA-cwg9-8mc4-hf7j.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/161-GHSA-cwg9-8mc4-hf7j.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,106 @@ +From 79486d4213150b4206f2d48257a75ddba77a33a9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Wed, 30 Sep 2026 10:07:08 -0400 +Subject: [PATCH] incusd/operations: Protect token operations from events and + cancellation +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Token operations are only readable by clients allowed to create the +matching token, but their events, including the secret, were sent to +any client able to view events in the default project. Such a client +could then reconstruct a certificate token and enroll an unrestricted +certificate. + +Cancelling an operation also skipped all checks when the operation type +has no associated permission, which includes token operations, so any +authenticated client could cancel them by UUID. + +Keep the metadata of token operations out of the event stream and +check access before cancelling an operation. Operations without a +specific permission can be cancelled by their requestor or by clients +able to edit the project. + +This addresses GHSA-cwg9-8mc4-hf7j (CVE pending) + +Signed-off-by: Stéphane Graber +--- + cmd/incusd/operations.go | 34 ++++++++++++++++++++++++----- + internal/server/operations/linux.go | 10 +++++++++ + 2 files changed, 38 insertions(+), 6 deletions(-) + +diff --git a/cmd/incusd/operations.go b/cmd/incusd/operations.go +index ded558ce82..c3af507348 100644 +--- a/cmd/incusd/operations.go ++++ b/cmd/incusd/operations.go +@@ -190,6 +190,31 @@ func operationCheckPermission(s *state.State, r *http.Request, op *operations.Op + return nil + } + ++// operationCheckCancelAccess checks whether the requestor may cancel the operation. ++func operationCheckCancelAccess(s *state.State, r *http.Request, op *operations.Operation) error { ++ err := operationCheckTokenAccess(s, r, op) ++ if err != nil { ++ return err ++ } ++ ++ objectType, _ := op.Permission() ++ if objectType != "" { ++ return operationCheckPermission(s, r, op) ++ } ++ ++ // Operations without a specific permission can be cancelled by their requestor. ++ if op.IsSameRequestor(r) { ++ return nil ++ } ++ ++ projectName := op.Project() ++ if projectName == "" { ++ projectName = api.ProjectDefaultName ++ } ++ ++ return s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(projectName), auth.EntitlementCanEdit) ++} ++ + // operationCheckViewAccess allows viewing an operation to callers with + // can_view_operations on its project or with the permission the operation + // itself requires, so that users scoped to a single resource can follow +@@ -371,12 +396,9 @@ func operationDelete(d *Daemon, r *http.Request) response.Response { + projectName = api.ProjectDefaultName + } + +- objectType, _ := op.Permission() +- if objectType != "" { +- err = operationCheckPermission(s, r, op) +- if err != nil { +- return response.SmartError(err) +- } ++ err = operationCheckCancelAccess(s, r, op) ++ if err != nil { ++ return response.SmartError(err) + } + + _, err = op.Cancel() +diff --git a/internal/server/operations/linux.go b/internal/server/operations/linux.go +index 4eec43bee1..9c489e7d91 100644 +--- a/internal/server/operations/linux.go ++++ b/internal/server/operations/linux.go +@@ -60,5 +60,15 @@ func (op *Operation) sendEvent(eventMessage any) { + return + } + ++ // Token operations carry a secret in their metadata, keep it out of the event stream. ++ if op.class == OperationClassToken { ++ apiOp, ok := eventMessage.(*api.Operation) ++ if ok { ++ opCopy := *apiOp ++ opCopy.Metadata = nil ++ eventMessage = &opCopy ++ } ++ } ++ + _ = op.events.Send(op.projectName, api.EventTypeOperation, eventMessage) + } +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/162-GHSA-5c64-j5m6-8433.patch incus-6.0.4/debian/patches/162-GHSA-5c64-j5m6-8433.patch --- incus-6.0.4/debian/patches/162-GHSA-5c64-j5m6-8433.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/162-GHSA-5c64-j5m6-8433.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,152 @@ +From 3ac4e2a18857581994b46c8fa3e38e288e644910 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Sun, 27 Sep 2026 19:41:14 -0400 +Subject: [PATCH] incusd: Check resource creation against the effective project +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Creation entitlements were checked against the requested project while +the resource was created in the project it is inherited from. A client +restricted to a project with a feature disabled could therefore create +networks, network ACLs, address sets, zones, profiles, custom volumes, +buckets, images and aliases in the default project. + +Resolve the effective project for creation entitlements the same way as +for existing objects. + +This addresses GHSA-5c64-j5m6-8433 (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/daemon.go | 48 ++++++++++++++++++++++++++++-- + cmd/incusd/images.go | 8 ++++- + internal/server/project/project.go | 20 +++++++++++++ + 3 files changed, 73 insertions(+), 3 deletions(-) + +diff --git a/cmd/incusd/daemon.go b/cmd/incusd/daemon.go +index 522abe9713..9011ce5020 100644 +--- a/cmd/incusd/daemon.go ++++ b/cmd/incusd/daemon.go +@@ -310,8 +310,23 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m + return projectName + } + +- // Object types that are always addressed in the requested project. +- if slices.Contains([]auth.ObjectType{auth.ObjectTypeProject, auth.ObjectTypeInstance}, objectType) { ++ // Instances are always addressed in the requested project. ++ if objectType == auth.ObjectTypeInstance { ++ return projectName ++ } ++ ++ // Project entitlements apply to the requested project, except for the creation of ++ // resources which may be inherited from the default project. ++ if objectType == auth.ObjectTypeProject && !slices.Contains([]auth.Entitlement{ ++ auth.EntitlementCanCreateImages, ++ auth.EntitlementCanCreateImageAliases, ++ auth.EntitlementCanCreateNetworks, ++ auth.EntitlementCanCreateNetworkACLs, ++ auth.EntitlementCanCreateNetworkZones, ++ auth.EntitlementCanCreateProfiles, ++ auth.EntitlementCanCreateStorageBuckets, ++ auth.EntitlementCanCreateStorageVolumes, ++ }, entitlement) { + return projectName + } + +@@ -334,6 +349,24 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m + return projectName + } + ++ // Resource creation is checked against the project the resource will be created in. ++ if objectType == auth.ObjectTypeProject { ++ switch entitlement { ++ case auth.EntitlementCanCreateImages, auth.EntitlementCanCreateImageAliases: ++ return project.ImageProjectFromRecord(p) ++ case auth.EntitlementCanCreateNetworks, auth.EntitlementCanCreateNetworkACLs: ++ return project.NetworkProjectFromRecord(p) ++ case auth.EntitlementCanCreateNetworkZones: ++ return project.NetworkZoneProjectFromRecord(p) ++ case auth.EntitlementCanCreateProfiles: ++ return project.ProfileProjectFromRecord(p) ++ case auth.EntitlementCanCreateStorageBuckets: ++ return project.StorageBucketProjectFromRecord(p) ++ case auth.EntitlementCanCreateStorageVolumes: ++ return project.StorageVolumeProjectFromRecord(p, db.StoragePoolVolumeTypeCustom) ++ } ++ } ++ + if objectType == auth.ObjectTypeProfile { + return project.ProfileProjectFromRecord(p) + } else if objectType == auth.ObjectTypeStorageBucket { +@@ -417,6 +450,16 @@ func allowPermission(objectType auth.ObjectType, entitlement auth.Entitlement, m + return response.SmartError(err) + } + ++ // Objects inherited from another project are served with the requested project's restrictions and ++ // data, so also require access to that project (project endpoints take it from their own path). ++ requestProjectName := request.ProjectParam(r) ++ if objectType != auth.ObjectTypeProject && objectName.Project() != "" && objectName.Project() != requestProjectName { ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(requestProjectName), auth.EntitlementCanView) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ } ++ + return response.EmptySyncResponse + } + } +diff --git a/cmd/incusd/images.go b/cmd/incusd/images.go +index 3384a8b032..d7714dc93c 100644 +--- a/cmd/incusd/images.go ++++ b/cmd/incusd/images.go +@@ -1006,8 +1006,14 @@ func imagesPost(d *Daemon, r *http.Request) response.Response { + + projectName := request.ProjectParam(r) + ++ // Images may be inherited from the default project, check against the effective project. ++ imageProjectName, err := projectutils.ImageProject(r.Context(), s.DB.Cluster, projectName) ++ if err != nil { ++ return response.SmartError(err) ++ } ++ + var userCanCreateImages bool +- err := s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(projectName), auth.EntitlementCanCreateImages) ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectProject(imageProjectName), auth.EntitlementCanCreateImages) + if err == nil { + userCanCreateImages = true + } else if !api.StatusErrorCheck(err, http.StatusForbidden) { +diff --git a/internal/server/project/project.go b/internal/server/project/project.go +index d58f66b327..c91c09353d 100644 +--- a/internal/server/project/project.go ++++ b/internal/server/project/project.go +@@ -248,6 +248,26 @@ func NetworkIntegrationAllowed(reqProjectConfig map[string]string, integrationNa + return slices.Contains(allowedRestrictedIntegrations, integrationName) + } + ++// ImageProject returns the effective project to use for images based on the requested project. ++func ImageProject(ctx context.Context, c *db.Cluster, projectName string) (string, error) { ++ var p *api.Project ++ err := c.Transaction(ctx, func(ctx context.Context, tx *db.ClusterTx) error { ++ dbProject, err := cluster.GetProject(ctx, tx.Tx(), projectName) ++ if err != nil { ++ return err ++ } ++ ++ p, err = dbProject.ToAPI(ctx, tx.Tx()) ++ ++ return err ++ }) ++ if err != nil { ++ return "", fmt.Errorf("Failed to load project %q: %w", projectName, err) ++ } ++ ++ return ImageProjectFromRecord(p), nil ++} ++ + // ImageProjectFromRecord returns the project name to use for the image based on the supplied project. + // If the project supplied has the "features.images" flag enabled then the project name is returned, + // otherwise the default project name is returned. +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/163-GHSA-9c46-v3hw-v6m8.patch incus-6.0.4/debian/patches/163-GHSA-9c46-v3hw-v6m8.patch --- incus-6.0.4/debian/patches/163-GHSA-9c46-v3hw-v6m8.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/163-GHSA-9c46-v3hw-v6m8.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,43 @@ +From 6b40a2e96cd30d0ee06333f16a657ec05faffd44 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Sat, 17 Oct 2026 09:41:24 -0400 +Subject: [PATCH] incusd/instance/lxc: Allocate an idmap when none is set at + start +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +A rebuild of a never-started unprivileged container deletes +volatile.idmap.next and volatile.last_state.idmap, and +volatile.idmap.current is only written on start, so all three are absent. +The start-time check only reallocated a map when one already existed, so +the container booted with an identity map, running as privileged. + +Allocate a map whenever an unprivileged instance has none. + +This addresses GHSA-9c46-v3hw-v6m8 (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/instance/drivers/driver_lxc.go | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/internal/server/instance/drivers/driver_lxc.go b/internal/server/instance/drivers/driver_lxc.go +index 2a8633d085..6bce923e4f 100644 +--- a/internal/server/instance/drivers/driver_lxc.go ++++ b/internal/server/instance/drivers/driver_lxc.go +@@ -1935,8 +1935,9 @@ func (d *lxc) startCommon() (string, []func() error, error) { + return "", nil, err + } + +- // Check if we need to change idmap. +- if nextMap != nil && d.state.OS.IdmapSet != nil && !d.state.OS.IdmapSet.Includes(nextMap) { ++ // Allocate a map when missing (e.g. after a rebuild of a never-started ++ // instance) or when the current one is outside the host's allowed range. ++ if d.state.OS.IdmapSet != nil && (nextMap == nil || !d.state.OS.IdmapSet.Includes(nextMap)) { + // Update the idmap. + idmapSet, base, err := d.findIdmap() + if err != nil { +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/164-GHSA-h2j8-4crh-vhfc.patch incus-6.0.4/debian/patches/164-GHSA-h2j8-4crh-vhfc.patch --- incus-6.0.4/debian/patches/164-GHSA-h2j8-4crh-vhfc.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/164-GHSA-h2j8-4crh-vhfc.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,107 @@ +From d5dbfda22467f11ef636e69f7127a997716e568d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 16 Oct 2026 02:36:23 -0400 +Subject: [PATCH] incusd/operations: Keep websocket secrets out of events and + queries +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Migration operations embed their per-connection websocket secrets in the +operation metadata, which was broadcast to every event listener and +returned by operation queries. Unlike exec and console, the migration +websocket authorizes on the secret alone, so a project event viewer could +read the secret and hijack an in-flight migration's data channels. + +Return the connection secrets only in the synchronous create response and +strip them from every other render. + +This addresses GHSA-h2j8-4crh-vhfc (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/operations/operations.go | 19 +++++++++++++++++++ + internal/server/operations/response.go | 3 ++- + 2 files changed, 21 insertions(+), 1 deletion(-) + +diff --git a/internal/server/operations/operations.go b/internal/server/operations/operations.go +index 025d420931..3a6fd4a844 100644 +--- a/internal/server/operations/operations.go ++++ b/internal/server/operations/operations.go +@@ -3,6 +3,7 @@ package operations + import ( + "context" + "fmt" ++ "maps" + "net/http" + "sync" + "time" +@@ -468,7 +469,19 @@ func (op *Operation) mayCancel() bool { + + // Render renders the operation structure. + // Returns URL of operation and operation info. ++// connectSecretKeys are the websocket operation metadata keys holding per-connection ++// secrets. They're returned to the operation's creator in the synchronous create ++// response but stripped from every other render, so they're never broadcast on the ++// event stream or exposed through operation queries. ++var connectSecretKeys = []string{api.SecretNameControl, api.SecretNameFilesystem, "fds"} ++ ++// Render renders the operation for events and API queries, with any websocket ++// connection secrets removed. + func (op *Operation) Render() (string, *api.Operation, error) { ++ return op.render(false) ++} ++ ++func (op *Operation) render(connectSecrets bool) (string, *api.Operation, error) { + // Setup the resource URLs + renderedResources := make(map[string][]string) + resources := op.resources +@@ -486,9 +499,20 @@ func (op *Operation) Render() (string, *api.Operation, error) { + renderedResources = tmpResources + } + +- // Local server name +- + op.lock.Lock() ++ ++ // Make a read-only copy of the metadata to avoid concurrent reads/writes. ++ metadata := map[string]any{} ++ maps.Copy(metadata, op.metadata) ++ ++ // Keep websocket connection secrets out of every render but the create response. ++ if !connectSecrets && op.class == OperationClassWebsocket { ++ for _, key := range connectSecretKeys { ++ delete(metadata, key) ++ } ++ } ++ ++ // Put together the response struct. + retOp := &api.Operation{ + ID: op.id, + Class: op.class.String(), +@@ -498,7 +522,7 @@ func (op *Operation) Render() (string, *api.Operation, error) { + Status: op.status.String(), + StatusCode: op.status, + Resources: renderedResources, +- Metadata: op.metadata, ++ Metadata: metadata, + MayCancel: op.mayCancel(), + } + +diff --git a/internal/server/operations/response.go b/internal/server/operations/response.go +index c4d70a489f..ed9fc92e4d 100644 +--- a/internal/server/operations/response.go ++++ b/internal/server/operations/response.go +@@ -27,7 +27,8 @@ func (r *operationResponse) Render(w http.ResponseWriter) error { + return err + } + +- url, md, err := r.op.Render() ++ // The create response is the one-time delivery of websocket secrets to the creator. ++ url, md, err := r.op.render(true) + if err != nil { + return err + } +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/165-GHSA-8r3h-vc36-6xpx.patch incus-6.0.4/debian/patches/165-GHSA-8r3h-vc36-6xpx.patch --- incus-6.0.4/debian/patches/165-GHSA-8r3h-vc36-6xpx.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/165-GHSA-8r3h-vc36-6xpx.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,138 @@ +From 6d56f5566324337713f14fbe85beba13c39a64eb Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Wed, 30 Sep 2026 09:56:49 -0400 +Subject: [PATCH] incusd/auth/tls: Restrict inherited reads to the + certificate's projects +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Restricted certificates get read-only access to resources in the +default project so that projects without a feature enabled can use the +inherited resources. That access was granted regardless of the project +being accessed, so a restricted client could read profiles, images, +volumes, buckets, networks, zones and their backups in the default +project by requesting it directly. + +Only allow inherited reads when the request is made through a project +the certificate is trusted for. Additionally hide network zone peer +keys from clients who can't edit the zone, as is done for networks. + +This addresses GHSA-8r3h-vc36-6xpx (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/network_zones.go | 29 ++++++++++++++++++++++++++++ + cmd/incusd/storage_buckets_backup.go | 2 +- + cmd/incusd/storage_volumes_backup.go | 2 +- + internal/server/auth/driver_tls.go | 4 ++-- + 4 files changed, 33 insertions(+), 4 deletions(-) + +diff --git a/cmd/incusd/network_zones.go b/cmd/incusd/network_zones.go +index 31de927a9f..57645cba85 100644 +--- a/cmd/incusd/network_zones.go ++++ b/cmd/incusd/network_zones.go +@@ -6,6 +6,7 @@ import ( + "fmt" + "net/http" + "net/url" ++ "strings" + + "github.com/gorilla/mux" + +@@ -209,6 +210,11 @@ func networkZonesGet(d *Daemon, r *http.Request) response.Response { + return response.InternalError(err) + } + ++ userCanEdit, err := s.Authorizer.GetPermissionChecker(r.Context(), r, auth.EntitlementCanEdit, auth.ObjectTypeNetworkZone) ++ if err != nil { ++ return response.InternalError(err) ++ } ++ + linkResults := make([]string, 0) + fullResults := make([]api.NetworkZone, 0) + for zoneName, projectName := range zoneNamesMap { +@@ -226,6 +232,10 @@ func networkZonesGet(d *Daemon, r *http.Request) response.Response { + netzoneInfo.UsedBy, _ = netzone.UsedBy() // Ignore errors in UsedBy, will return nil. + netzoneInfo.Project = projectName + ++ if !userCanEdit(auth.ObjectNetworkZone(projectName, zoneName)) { ++ networkZoneStripSecrets(netzoneInfo) ++ } ++ + if clauses != nil && len(clauses.Clauses) > 0 { + match, err := filter.Match(*netzoneInfo, *clauses) + if err != nil { +@@ -452,9 +462,28 @@ func networkZoneGet(d *Daemon, r *http.Request) response.Response { + return response.SmartError(err) + } + ++ // Only editors get to see the peer keys. ++ err = s.Authorizer.CheckPermission(r.Context(), r, auth.ObjectNetworkZone(projectName, zoneName), auth.EntitlementCanEdit) ++ if err != nil { ++ if !api.StatusErrorCheck(err, http.StatusForbidden) { ++ return response.SmartError(err) ++ } ++ ++ networkZoneStripSecrets(info) ++ } ++ + return response.SyncResponseETag(true, info, netzone.Etag()) + } + ++// networkZoneStripSecrets removes the peer keys from the zone config. ++func networkZoneStripSecrets(info *api.NetworkZone) { ++ for key := range info.Config { ++ if strings.HasPrefix(key, "peers.") && strings.HasSuffix(key, ".key") { ++ delete(info.Config, key) ++ } ++ } ++} ++ + // swagger:operation PATCH /1.0/network-zones/{zone} network-zones network_zone_patch + // + // Partially update the network zone +diff --git a/cmd/incusd/storage_buckets_backup.go b/cmd/incusd/storage_buckets_backup.go +index bc81037bc5..1f6cc00929 100644 +--- a/cmd/incusd/storage_buckets_backup.go ++++ b/cmd/incusd/storage_buckets_backup.go +@@ -47,7 +47,7 @@ var storagePoolBucketBackupCmd = APIEndpoint{ + var storagePoolBucketBackupsExportCmd = APIEndpoint{ + Path: "storage-pools/{poolName}/buckets/{bucketName}/backups/{backupName}/export", + +- Get: APIEndpointAction{Handler: storagePoolBucketBackupExportGet, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanView, "poolName", "bucketName", "location")}, ++ Get: APIEndpointAction{Handler: storagePoolBucketBackupExportGet, AccessHandler: allowPermission(auth.ObjectTypeStorageBucket, auth.EntitlementCanManageBackups, "poolName", "bucketName", "location")}, + } + + // swagger:operation GET /1.0/storage-pools/{poolName}/buckets/{bucketName}/backups storage storage_pool_buckets_backups_get +diff --git a/cmd/incusd/storage_volumes_backup.go b/cmd/incusd/storage_volumes_backup.go +index 4efb821a69..8f0ba754db 100644 +--- a/cmd/incusd/storage_volumes_backup.go ++++ b/cmd/incusd/storage_volumes_backup.go +@@ -49,7 +49,7 @@ var storagePoolVolumeTypeCustomBackupCmd = APIEndpoint{ + var storagePoolVolumeTypeCustomBackupExportCmd = APIEndpoint{ + Path: "storage-pools/{poolName}/volumes/{type}/{volumeName}/backups/{backupName}/export", + +- Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupExportGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanView, "poolName", "type", "volumeName", "location")}, ++ Get: APIEndpointAction{Handler: storagePoolVolumeTypeCustomBackupExportGet, AccessHandler: allowPermission(auth.ObjectTypeStorageVolume, auth.EntitlementCanManageBackups, "poolName", "type", "volumeName", "location")}, + } + + // swagger:operation GET /1.0/storage-pools/{poolName}/volumes/{type}/{volumeName}/backups storage storage_pool_volumes_type_backups_get +diff --git a/internal/server/auth/driver_tls.go b/internal/server/auth/driver_tls.go +index 94b9d9157c..7f5c12d5f9 100644 +--- a/internal/server/auth/driver_tls.go ++++ b/internal/server/auth/driver_tls.go +@@ -92,8 +92,8 @@ func (t *TLS) CheckPermission(ctx context.Context, r *http.Request, object Objec + return nil + } + +- // Also allow read-only access to inherited resources. +- if object.Project() == api.ProjectDefaultName && entitlement == EntitlementCanView && slices.Contains([]ObjectType{ObjectTypeImage, ObjectTypeProfile, ObjectTypeStorageVolume, ObjectTypeStorageBucket, ObjectTypeNetwork, ObjectTypeNetworkZone}, object.Type()) { ++ // Also allow read-only access to inherited resources, but only when accessed through a project the certificate is trusted for. ++ if object.Project() == api.ProjectDefaultName && entitlement == EntitlementCanView && slices.Contains(projectNames, details.ProjectName) && slices.Contains([]ObjectType{ObjectTypeImage, ObjectTypeProfile, ObjectTypeStorageVolume, ObjectTypeStorageBucket, ObjectTypeNetwork, ObjectTypeNetworkZone}, object.Type()) { + return nil + } + +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/166-GHSA-x6pw-mh5w-pq73.patch incus-6.0.4/debian/patches/166-GHSA-x6pw-mh5w-pq73.patch --- incus-6.0.4/debian/patches/166-GHSA-x6pw-mh5w-pq73.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/166-GHSA-x6pw-mh5w-pq73.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,33 @@ +From 196762a4aee0057a88cabfe9e817192c5b189bf5 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Mon, 26 Oct 2026 17:36:23 -0400 +Subject: [PATCH] incusd: Reject null snapshot entries on backup import +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses GHSA-x6pw-mh5w-pq73 (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + internal/server/backup/backup_config_utils.go | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/internal/server/backup/backup_config_utils.go b/internal/server/backup/backup_config_utils.go +index 7f6b5b8e32..bea66334c6 100644 +--- a/internal/server/backup/backup_config_utils.go ++++ b/internal/server/backup/backup_config_utils.go +@@ -191,6 +191,10 @@ func UpdateInstanceConfig(c *db.Cluster, b Info, mountPath string) error { + } + + for _, snapshot := range backup.Snapshots { ++ if snapshot == nil { ++ return fmt.Errorf("Bad snapshot definition found in index") ++ } ++ + updateRootDevicePool(snapshot.Devices, pool.Name) + updateRootDevicePool(snapshot.ExpandedDevices, pool.Name) + } +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/167-GHSA-pf5w-m8m6-5265.patch incus-6.0.4/debian/patches/167-GHSA-pf5w-m8m6-5265.patch --- incus-6.0.4/debian/patches/167-GHSA-pf5w-m8m6-5265.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/167-GHSA-pf5w-m8m6-5265.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,73 @@ +From 9b266b2bc8c7b02d7f7b0e2b5d8a28f18994b0bf Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Tue, 20 Oct 2026 15:19:55 -0400 +Subject: [PATCH] incusd: Fix cross-project disclosure in recursive and + all-projects listings +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +This addresses GHSA-pf5w-m8m6-5265 (CVE pending) + +Signed-off-by: Stéphane Graber +Rebased-by: Mathias Gibbens +--- + cmd/incusd/profiles.go | 16 +++++++- + cmd/incusd/storage_buckets.go | 74 +++++++++++++++++++++++++++-------- + 2 files changed, 71 insertions(+), 19 deletions(-) + +diff --git a/cmd/incusd/profiles.go b/cmd/incusd/profiles.go +index bd178ce1d1..597cc90113 100644 +--- a/cmd/incusd/profiles.go ++++ b/cmd/incusd/profiles.go +@@ -224,7 +224,13 @@ func profilesGet(d *Daemon, r *http.Request) response.Response { + } + + for _, profile := range profiles { +- if !userHasPermission(auth.ObjectProfile(p.Name, profile.Name)) { ++ // In all-projects mode each profile must be checked against its own project. ++ objectProjectName := p.Name ++ if allProjects { ++ objectProjectName = profile.Project ++ } ++ ++ if !userHasPermission(auth.ObjectProfile(objectProjectName, profile.Name)) { + continue + } + +@@ -256,7 +262,13 @@ func profilesGet(d *Daemon, r *http.Request) response.Response { + } + } else { + for _, profile := range profiles { +- if !userHasPermission(auth.ObjectProfile(p.Name, profile.Name)) { ++ // In all-projects mode each profile must be checked against its own project. ++ objectProjectName := p.Name ++ if allProjects { ++ objectProjectName = profile.Project ++ } ++ ++ if !userHasPermission(auth.ObjectProfile(objectProjectName, profile.Name)) { + continue + } + +diff --git a/cmd/incusd/storage_buckets.go b/cmd/incusd/storage_buckets.go +index 1259d43d69..7edb1a081d 100644 +--- a/cmd/incusd/storage_buckets.go ++++ b/cmd/incusd/storage_buckets.go +@@ -251,7 +257,13 @@ func storagePoolBucketsGet(d *Daemon, r *http.Request) response.Response { + location = bucket.Location + } + +- if !userHasPermission(auth.ObjectStorageBucket(requestProjectName, poolName, bucket.Name, location)) { ++ // In all-projects mode each bucket must be checked against its own project. ++ objectProjectName := requestProjectName ++ if allProjects { ++ objectProjectName = bucket.Project ++ } ++ ++ if !userHasPermission(auth.ObjectStorageBucket(objectProjectName, poolName, bucket.Name, location)) { + continue + } + +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/168-GHSA-f7xp-22ph-v444.patch incus-6.0.4/debian/patches/168-GHSA-f7xp-22ph-v444.patch --- incus-6.0.4/debian/patches/168-GHSA-f7xp-22ph-v444.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/168-GHSA-f7xp-22ph-v444.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,38 @@ +From 0844690eba3aa74846d4ce895626381f24890834 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 16 Oct 2026 02:24:27 -0400 +Subject: [PATCH] incusd/dev_incus: Pin image export to the instance's project +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The guest-API image export passed the guest-controllable project query +parameter straight through to imageExport, letting a container with +security.guestapi.images export any public or cached image of any +project. Force the instance's own project. + +This addresses GHSA-f7xp-22ph-v444 (CVE pending) + +Signed-off-by: Stéphane Graber +--- + cmd/incusd/dev_incus.go | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/cmd/incusd/dev_incus.go b/cmd/incusd/dev_incus.go +index 4e6e6d04f7..78ea4de8eb 100644 +--- a/cmd/incusd/dev_incus.go ++++ b/cmd/incusd/dev_incus.go +@@ -108,6 +108,11 @@ var devIncusImageExport = devIncusHandler{"/1.0/images/{fingerprint}/export", fu + // Use by security checks to distinguish /dev/incus vs REST APs + r.RemoteAddr = "@dev_incus" + ++ // Pin the project to the instance's own so the guest can't export other projects' images. ++ query := r.URL.Query() ++ query.Set("project", c.Project().Name) ++ r.URL.RawQuery = query.Encode() ++ + resp := imageExport(d, r) + + err := resp.Render(w) +-- +2.47.3 diff -Nru incus-6.0.4/debian/patches/169-GHSA-9f75-5fjw-5jvv.patch incus-6.0.4/debian/patches/169-GHSA-9f75-5fjw-5jvv.patch --- incus-6.0.4/debian/patches/169-GHSA-9f75-5fjw-5jvv.patch 1970-01-01 00:00:00.000000000 +0000 +++ incus-6.0.4/debian/patches/169-GHSA-9f75-5fjw-5jvv.patch 2026-10-29 22:14:24.000000000 +0000 @@ -0,0 +1,52 @@ +From 750b4090c0ecbe7a5729a75c86e06a5a7f21965b Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?St=C3=A9phane=20Graber?= +Date: Fri, 16 Oct 2026 02:25:29 -0400 +Subject: [PATCH] incusd/events: Restrict network-acl events to privileged + viewers +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +OVN ACL log events are broadcast with an empty project, so the per-project +binding and permission checks in the event server are skipped and any +client with view-events on a single project received every project's ACL +logs. The project can't be recovered from the raw OVN syslog text, so treat +network-acl as a privileged event type like logging. + +This addresses GHSA-9f75-5fjw-5jvv (CVE pending) + +Signed-off-by: Stéphane Graber +--- + cmd/incusd/events.go | 10 +++++++--- + 1 file changed, 7 insertions(+), 3 deletions(-) + +diff --git a/cmd/incusd/events.go b/cmd/incusd/events.go +index bf154efed4..b639014c64 100644 +--- a/cmd/incusd/events.go ++++ b/cmd/incusd/events.go +@@ -23,7 +23,7 @@ import ( + + var ( + eventTypes = []string{api.EventTypeLogging, api.EventTypeOperation, api.EventTypeLifecycle, api.EventTypeNetworkACL} +- privilegedEventTypes = []string{api.EventTypeLogging} ++ privilegedEventTypes = []string{api.EventTypeLogging, api.EventTypeNetworkACL} + ) + + var eventsCmd = APIEndpoint{ +@@ -103,8 +103,12 @@ func eventsSocket(s *state.State, r *http.Request, w http.ResponseWriter) error + } + } + +- if slices.Contains(types, api.EventTypeLogging) && !canViewPrivilegedEvents { +- return api.StatusErrorf(http.StatusForbidden, "Forbidden") ++ if !canViewPrivilegedEvents { ++ for _, entry := range types { ++ if slices.Contains(privilegedEventTypes, entry) { ++ return api.StatusErrorf(http.StatusForbidden, "Forbidden") ++ } ++ } + } + + l := logger.AddContext(logger.Ctx{"remote": r.RemoteAddr}) +-- +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-09-25 14:26:49.000000000 +0000 +++ incus-6.0.4/debian/patches/series 2026-10-29 22:23:26.000000000 +0000 @@ -63,3 +63,20 @@ 153-GHSA-jh4v-j34r-2mgh.patch 154-GHSA-mfwv-x733-9446.patch 155-GHSA-h85r-gjgx-g2rv-GHSA-27q7-qwhm-c34p.patch +156-cherry-pick-skip-processing-limits-vm.patch +157a-cherry-pick-strip-unsafe-symlinks.patch +157b-cherry-pick-allow-expected-symlinks.patch +157c-GHSA-5pgx-q9xx-j3qv.patch +158-GHSA-jcw9-v2ff-8m79.patch +159a-cherry-pick-CheckTrustStateIssuer.patch +159b-GHSA-xg75-xf37-cph9.patch +160-GHSA-p45f-gqqj-6qq3.patch +161-GHSA-cwg9-8mc4-hf7j.patch +162-GHSA-5c64-j5m6-8433.patch +163-GHSA-9c46-v3hw-v6m8.patch +164-GHSA-h2j8-4crh-vhfc.patch +165-GHSA-8r3h-vc36-6xpx.patch +166-GHSA-x6pw-mh5w-pq73.patch +167-GHSA-pf5w-m8m6-5265.patch +168-GHSA-f7xp-22ph-v444.patch +169-GHSA-9f75-5fjw-5jvv.patch