Jan 23, 2026·6 min read·76 visits
Incus (an LXD fork) trusted user-supplied paths in container image templates too much. By crafting a malicious image with a specific `metadata.yaml`, an attacker can trick the root-privileged Incus daemon into reading or writing files outside the container rootfs. This is trivially exploitable for host-level Remote Code Execution (RCE).
A critical path traversal and symbolic link vulnerability in the Incus container manager allows privileged container users to escape confinement and execute arbitrary code as root on the host system.
In the world of system containers, we often conflate "containment" with "security boundary." Incus, the community-driven fork of LXD, is a powerful tool for managing system containers that feel like full virtual machines. One of its creature comforts is image templating. When you launch a container, Incus doesn't just unpack a tarball; it looks at a metadata.yaml file to perform last-mile customization. It might update /etc/hosts, set a hostname, or inject cloud-init configurations.
Here is the catch: The Incus daemon performs these file operations. The Incus daemon runs as root. And the instructions for what to write and where to write it come from the container image itself.
This architecture creates a classic "Confused Deputy" scenario. The daemon is helpful, privileged, and obedient. If the image asks it to update a file, it does so. The security of the entire host relies on the daemon strictly checking that it isn't being tricked into writing outside the container's sandbox. In CVE-2026-23954, those checks failed spectacularly.
The vulnerability lies in internal/server/instance/drivers/driver_lxc.go, specifically within the templateApplyNow function. This function is responsible for iterating through the templates defined in an image's metadata and applying them.
Root Cause Analysis reveals a classic Path Traversal (CWE-22) combined with Improper Link Resolution. When processing a template, the code takes a source file (from the image) and copies it to a target path (inside the container). The logic relied on standard string manipulation to determine these paths.
The fatal error was assuming that filepath.Join is sufficient security. While filepath.Join cleans up dirty paths (resolving ../), it operates purely on the lexical path string. It does not check the physical reality of the filesystem. If an attacker places a symbolic link inside the container image that points to the host's root (/), filepath.Join sees a valid path inside the container, but the operating system follows the symlink out to the host filesystem during the file write operation.
Let's look at the vulnerable Go code vs. the secure implementation. This highlights why modern file system APIs are critical for security.
The Vulnerable Code:
The original code constructed the full path by joining the container's rootfs path with the target template path. It then handed this string directly to os.Create.
// Vulnerable logic in driver_lxc.go
// d.RootfsPath() is the container's safe jail (e.g., /var/lib/incus/containers/foo/rootfs)
// tplPath comes from the attacker-controlled metadata.yaml
fullpath := filepath.Join(d.RootfsPath(), strings.TrimLeft(tplPath, "/"))
// CRITICAL FLAW: If a component of tplPath is a symlink to /,
// os.Create follows it out of the jail.
w, err = os.Create(fullpath)The Fix (Go 1.24 Magic):
The patch leverages a new feature in Go 1.24: os.Root. This provides a capability-based file system handle. Instead of operating on global strings, we open a "root" handle to the container directory. Any operations performed relative to this handle are enforced by the OS (using mechanisms like openat2 with RESOLVE_BENEATH) to ensure they cannot escape that directory tree, regardless of symlinks or ../ trickery.
// Patched logic
rootPath, err := os.OpenRoot(d.RootfsPath())
if err != nil {
return err
}
defer rootPath.Close()
// We strip the leading slash but use the root handle to create the file.
// If relPath traverses out via symlinks, this call explicitly fails.
relPath := strings.TrimLeft(tplPath, "/")
w, err = rootPath.Create(relPath)To exploit this, we don't need memory corruption or complex heap spraying. We just need to be able to import a custom image. This is a privilege usually granted to the incus group.
Step 1: The Trap (Symbolic Link) We create a malicious root filesystem containing a symlink that points to the host's root directory. Inside our image tarball:
ln -s / rootfs/realrootStep 2: The Map (metadata.yaml)
We craft the metadata.yaml to define a template action. We tell Incus: "Hey, when you start this container, please take this innocent file and write it to /realroot/proc/sys/kernel/core_pattern."
Because of the symlink we created in Step 1, /realroot/ actually resolves to the host's root directory. Incus thinks it is writing to /var/lib/incus/containers/poc/rootfs/realroot/..., but the kernel redirects the write to /proc/sys/kernel/core_pattern on the host.
Step 3: The Payload (RCE)
We overwrite core_pattern to execute a shell script whenever a program crashes. This is a standard container escape technique.
templates:
# The exploit target: Host's core_pattern
/realroot/proc/sys/kernel/core_pattern:
when: [start]
template: payload.tplStep 4: Execution
We import the image and launch it. The moment the container starts, the Incus daemon processes the template, traversing our symlink and overwriting the host's kernel configuration. We then crash a process (e.g., sleep 10 & kill -SIGSEGV %1), causing the kernel to trigger our payload as root.
The impact here is Critical (CVSS 8.7). While the score is slightly tempered because you need initial access to the incus group (or similar privileges to create images), the result is total system compromise.
This vulnerability allows for:
/etc/shadow, /etc/ld.so.preload, /usr/bin/*).../ traversal (e.g., template: ../../../../../etc/shadow). Incus will read the host file and copy it into our container, allowing us to steal password hashes or SSH keys.This effectively blurs the line between "container admin" and "host admin." If you run a multi-tenant environment where users are allowed to bring their own images, you are vulnerable.
The fix was released in Incus 6.21.0 (Feature release) and Incus 6.0.6 (LTS release). The primary mitigation is to upgrade immediately.
If you cannot upgrade, you must restrict access to the incus group. Treat any user with incus group membership as if they already have root access to the host—because, with this exploit, they effectively do. Additionally, avoid importing images from untrusted sources or public remotes that you do not control.
For developers, the lesson is clear: Never trust filepath strings. If you are writing file manipulation code in Go, upgrade to 1.24 and use os.OpenRoot for any directory-constrained operations. The filesystem state is volatile; checking a path string before use is a Time-of-Check Time-of-Use (TOCTOU) race condition waiting to happen. Let the OS kernel enforce the boundaries via file descriptors.
CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Incus Linux Containers | <= 6.0.5 | 6.0.6 |
Incus Linux Containers | <= 6.20.0 | 6.21.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 (Path Traversal) |
| CVSS v3.1 | 8.7 (High) |
| Attack Vector | Adjacent Network (AV:A) |
| Impact | Confidentiality High, Integrity High |
| Exploit Status | PoC Available |
| Prerequisites | Container launch permissions (incus group) |
The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Grav CMS prior to version 1.7.53 and 2.0.0-rc.8 is vulnerable to an unauthenticated remote denial of service (DoS) vulnerability. By supplying crafted query parameters with extremely large dimensions to image assets, remote unauthenticated attackers can force the server to allocate massive amounts of system memory, leading to kernel Out-Of-Memory (OOM) termination of web worker processes.
CVE-2026-53657 is a local privilege escalation vulnerability in Lima (lima-vm/lima) affecting versions prior to 2.1.3 when configured with the QEMU driver. The guest agent daemon, running as root, creates its communication socket `/run/lima-guestagent.sock` with world-writable permissions (0777). This allows unprivileged local users to command the agent to establish arbitrary tunnels, including to privileged local UNIX sockets (like D-Bus). Because the target daemon authenticates the incoming connection using the credentials of the root-owned guest agent (via SO_PEERCRED), unprivileged users can perform root operations, resulting in complete guest VM compromise.
An unauthenticated Denial of Service vulnerability exists in the s2n-quic library's CryptoStream reassembler due to a lack of buffer limits on out-of-order cryptographic frames. An attacker can transmit a crafted CRYPTO frame with an extremely high offset and nominal payload, forcing the receiver to execute unbounded memory allocations and causing service crashes.
A JNDI Injection and Deserialization Gadget vulnerability exists in mchange-commons-java prior to version 0.6.0. The com.mchange.v2.naming.JavaBeanObjectFactory component permits arbitrary class instantiation and setter invocation, allowing attackers to perform Server-Side Request Forgery (SSRF) and remote class loading.
SurrealDB versions supporting element-level SELECT permissions on arrays are vulnerable to a logical authorization bypass. Due to an index-shifting error during array filtration, restricted elements can skip permission checks and leak to unauthorized record users.
CVE-2026-12243 is a path traversal vulnerability in the Natural Language Toolkit (NLTK) version 3.9.4. The flaw exists because the input validation routine fails to account for percent-encoded directory traversal sequences like '..%2f' before passing them to urllib.request.url2pathname(), which decodes them into active traversal sequences.