Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

GHSA-9J25-MM2H-2F76: CVE-2026-48750: Arbitrary File Write on Host via exec-output Symlink in Canonical LXD and Incus

CVE-2026-48750: Arbitrary File Write on Host via exec-output Symlink in Canonical LXD and Incus Vulnerability ID: GHSA-9J25-MM2H-2F76 CVSS Score: 9.9 Published: 2026-11-06 Canonical LXD and LXC Incus contain an arbi

CVE-2026-48750: Arbitrary File Write on Host via exec-output Symlink in Canonical LXD and Incus

Vulnerability ID: GHSA-9J25-MM2H-2F76
CVSS Score: 9.9
Published: 2026-11-06

Canonical LXD and LXC Incus contain an arbitrary file write vulnerability (CWE-73 / CWE-59) in the /1.0/instances/<name>/exec REST API endpoint. When an instance is unpacked from an image archive, top-level symbolic links are extracted directly onto the host filesystem within the instance storage directory. If a crafted image includes a symbolic link named exec-output pointing to an arbitrary host path, invoking the execution endpoint with record-output: true causes the privileged host daemon to follow the symlink and write command output files directly to the target host directory. An authenticated attacker with permissions to import images and execute commands can exploit this flaw to write controlled content to sensitive host directories such as /etc/cron.d, resulting in arbitrary code execution on the host as root.

TL;DR

An unsanitized symlink named exec-output inside a crafted container image allows authenticated LXD and Incus users to write arbitrary command output to any directory on the host filesystem when invoking the /exec endpoint with record-output: true, leading to host-level privilege escalation and arbitrary code execution as root.

⚠️ Exploit Status: POC

Technical Details

  • CVE Identifier: CVE-2026-48750 (GHSA-9j25-mm2h-2f76)
  • Weakness Class: CWE-73: External Control of File Name or Path / CWE-59: Symlink Following
  • CVSS v3.1 Score: 9.9 Critical (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
  • EPSS Score: 0.78% (56.22nd Percentile)
  • Attack Vector: Authenticated REST API (/1.0/instances//exec with record-output: true)
  • Security Impact: Arbitrary file creation on host leading to root code execution (Container Escape)
  • Exploit Maturity: Public Proof-of-Concept (PoC) Available
  • CISA KEV Status: Not Listed

Affected Systems

  • Canonical LXD
  • LXC Incus
  • LXD (4.0 LTS): >= 4.0.0, < 4.0.11 (Fixed in: 4.0.11)
  • LXD (5.0 LTS): >= 5.0.0, < 5.0.7 (Fixed in: 5.0.7)
  • LXD (5.x / 5.21 LTS): >= 5.1.0, < 5.21.5 (Fixed in: 5.21.5)
  • LXD (6.x): >= 6.0.0, < 6.9.0 (Fixed in: 6.9.0)
  • Incus: < 7.2.0 (Fixed in: 7.2.0)

Code Analysis

Commit: 124f901

lxd/instance_exec: Confine opening of exec outputs

--- a/lxd/instance_exec.go
+++ b/lxd/instance_exec.go
@@ -718,24 +716,24 @@ func instanceExecPost(d *Daemon, r *http.Request) response.Response {
        var stdout, stderr *os.File

        if post.RecordOutput {
-           // Ensure exec-output directory exists
-           execOutputDir := inst.ExecOutputPath()
-           err = os.Mkdir(execOutputDir, 0600)
-           if err != nil && !errors.Is(err, fs.ErrExist) {
+           execOutputRoot, err := inst.OpenExecOutput()
+           if err != nil {
                return err
            }

+           defer func() { _ = execOutputRoot.Close() }()
+
            // Prepare stdout and stderr recording.
            stdoutFile := "exec_" + opID + ".stdout"
-           stdout, err = os.OpenFile(filepath.Join(execOutputDir, stdoutFile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+           stdout, err = execOutputRoot.OpenFile(stdoutFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
            if err != nil {
                return err
            }

            defer func() { _ = stdout.Close() }()

            stderrFile := "exec_" + opID + ".stderr"
-           stderr, err = os.OpenFile(filepath.Join(execOutputDir, stderrFile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+           stderr, err = execOutputRoot.OpenFile(stderrFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
            if err != nil {
                return err
            }

Commit: 254b85c

lxd/instance/drivers/common: Helper to ensure opening of instance paths is confined

--- a/lxd/instance/drivers/driver_common.go
+++ b/lxd/instance/drivers/driver_common.go
@@ -4,6 +4,7 @@ import (
    "context"
    "errors"
    "fmt"
+   "io/fs"
    "net/http"
    "os"
    "path/filepath"
@@ -396,6 +397,64 @@ func (d *common) Path() string {
    return storagePools.InstancePath(d.dbType, d.project.Name, d.name, d.isSnapshot)
 }

+// openSubPath opens name as a confined [os.Root] beneath [common.Path], rejecting any attempt to
+// escape the instance path.
+func (d *common) openSubPath(name string) (*os.Root, error) {
+   parent, err := os.OpenRoot(d.Path())
+   if err != nil {
+       return nil, err
+   }
+
+   defer func() { _ = parent.Close() }()
+
+   root, err := parent.OpenRoot(name)
+   if err != nil {
+       return nil, fmt.Errorf("Failed opening %q for instance %q (path %s): %w", name, d.Name(), d.Path(), err)
+   }
+
+   return root, nil
+}
+
+// openOrCreateSubPath is like openSubPath but also creates the name if it does not exist.
+func (d *common) openOrCreateSubPath(name string, mode os.FileMode) (*os.Root, error) {
+   parent, err := os.OpenRoot(d.Path())
+   if err != nil {
+       return nil, err
+   }
+
+   defer func() { _ = parent.Close() }()
+
+   err = parent.Mkdir(name, mode)
+   if err != nil && !errors.Is(err, fs.ErrExist) {
+       return nil, fmt.Errorf("Failed creating %q for instance %q (path %s): %w", name, d.Name(), d.Path(), err)
+   }
+
+   root, err := parent.OpenRoot(name)
+   if err != nil {
+       return nil, fmt.Errorf("Failed opening %q for instance %q (path %s): %w", name, d.Name(), d.Path(), err)
+   }
+
+   return root, nil
+}
+
+// OpenRootfs opens the instance's rootfs directory as a confined *os.Root.
+// Caller must close the returned Root.
+func (d *common) OpenRootfs() (*os.Root, error) {
+   return d.openSubPath("rootfs")
+}
+
+// OpenExecOutput opens the instance's exec-output directory as a confined *os.Root, creating it if absent.
+// Caller must close the returned Root.
+func (d *common) OpenExecOutput() (*os.Root, error) {
+   return d.openOrCreateSubPath("exec-output", 0700)
+}
+
+// OpenTemplates opens the instance's templates directory as a confined *os.Root, creating it if absent.
+// Caller must close the returned Root.
+func (d *common) OpenTemplates() (*os.Root, error) {
+   return d.openOrCreateSubPath("templates", 0700)
+}
+
 // ExecOutputPath returns the instance's exec output path.
 func (d *common) ExecOutputPath() string {
    return filepath.Join(d.Path(), "exec-output")

Commit: 4cd6334

lxd/instance/drivers/common: Helper to ensure opening of instance paths is confined (cherry-pick)

Commit: 937b761

lxd/instance_exec: Confine opening of exec outputs (cherry-pick)

Commit: bb0f252

lxd/instance_exec: Confine opening of exec outputs (cherry-pick)

Exploit Details

Mitigation Strategies

  • Upgrade Canonical LXD or LXC Incus to the patched release corresponding to your deployment branch.
  • Restrict access to the LXD/Incus UNIX control socket and TLS REST API endpoints to trusted host administrators.
  • Audit instance directories across all storage pools for symbolic links named exec-output.
  • Monitor host configuration directories (such as /etc/cron.d and /etc/profile.d) for files matching the pattern exec_.stdout or exec_.stderr.

Remediation Steps:

  1. Identify the currently running LXD or Incus version using lxd --version or incus --version.
  2. For Snap-based LXD deployments, run sudo snap refresh lxd to update to version 5.21.5, 5.0.7, 4.0.11, or 6.9.0+.
  3. For Incus deployments, upgrade the incus package via your distribution package manager to version 7.2.0 or higher.
  4. Verify that no existing container directories contain symbolic links for exec-output using find /var/lib/lxd/containers/ -maxdepth 2 -name "exec-output" -type l -ls.

References

Read the full report for GHSA-9J25-MM2H-2F76 on our website for more details including interactive diagrams and full exploit analysis.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.