Jan 23, 2026·6 min read·72 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.
An uncontrolled resource consumption vulnerability exists in the Scala-based http4s-blaze-server package of the http4s/blaze library. The vulnerability allows remote, unauthenticated attackers to cause an Out of Memory Error (OOM) and JVM crash by streaming a continuous sequence of small or empty WebSocket continuation frames with the FIN bit set to 0. This bypasses typical payload size checks because of the JVM's per-object allocation overhead, leading to rapid heap exhaustion with minimal network bandwidth.
A critical path traversal vulnerability has been identified in the OpenList Go-based backend package. The vulnerability exists within the batch rename handler because the application does not validate the source filename parameter before constructing filesystems paths. This omission allows authenticated users to escape their designated directory and rename files in sibling paths.
OpenList version 4.2.3 and prior is vulnerable to an authorization bypass and metadata leakage. When configured with the Bleve search engine backend, OpenList fails to perform separator-aware path matching when validating tenant containment. This allows authenticated users to access sibling directories sharing similar name prefixes. Furthermore, the search backend returns unfiltered global result counts, leaking existence verification data of unauthorized files via side-channel analysis.
An authorization bypass vulnerability in OpenList version 4.2.3 and below allows authenticated users to read arbitrary files outside of their designated base directories due to an insecure path prefix check using Go's standard strings.HasPrefix function.
A security policy bypass vulnerability exists in the AWS API MCP Server (awslabs-aws-api-mcp-server) from version 0.2.13 through 1.3.46. When the server fails to load the read-only operations index during startup (due to transient network failures, file permission issues, or other exceptions), it logs a warning but continues running in an insecure, degraded state. Under this condition, the security policy engine fails open, silently skipping all subsequent security checks and consent prompts for the lifetime of the process. This permits unauthorized mutating AWS CLI commands to execute via indirect prompt injection attacks.
An incomplete escaping vulnerability in the npm package 'shescape' allows unauthenticated users to trigger dynamic shell expansions, absolute path disclosure, and command block break-outs on Unix and Windows systems.