Nov 7, 2026·10 min read·0 visits
An argument injection flaw in Canonical LXD's backup compression handling allows authenticated users to pass arbitrary command-line flags to host compression utilities like zstd. By abusing flags such as -o, an attacker can write files from a container directly to the host's /etc/cron.d/ directory, escaping the container and executing arbitrary code as root on the host.
GHSA-fmc8-p6q7-75cc (associated with CVE-2026-48755) is a critical argument injection vulnerability in Canonical LXD (github.com/canonical/lxd). The vulnerability occurs during instance and storage volume backup operations when processing the user-supplied compression_algorithm configuration parameter. While LXD validates the primary executable token against an allowlist of supported compression binaries, it fails to sanitize or restrict subsequent tokens parsed via shellquote.Split. These additional arguments are passed directly to exec.Command. An authenticated attacker with privileges to trigger backups can inject command-line flags for utilities like zstd to read staged files from a container filesystem and write them to arbitrary paths on the underlying host filesystem, such as /etc/cron.d/, achieving root-level arbitrary code execution on the host.
Canonical LXD is a system container and virtual machine manager that provides a REST API for managing instances, storage volumes, and network configurations. Among its management capabilities, LXD exposes endpoints—such as /1.0/instances/{name}/backups and corresponding storage volume backup endpoints—that allow authenticated users to export compressed archives of container filesystems and virtual machine disks.
During the backup creation process, the API accepts a JSON payload containing a compression_algorithm field. This setting allows users to override the default server compression method and specify utilities such as gzip, bzip2, xz, lz4, or zstd. Because the LXD daemon (lxd) executes with root privileges on the host operating system, any external processes spawned to handle backup compression inherit this privileged execution context unless explicitly confined.
GHSA-fmc8-p6q7-75cc (tracked upstream in commit messages as CVE-2026-48755 and related to the Linux Containers Incus advisory GHSA-v6mj-8pf4-hhw4) is an argument injection vulnerability (CWE-88) in LXD's compression command parser. When validating the compression_algorithm string, LXD splits the input into shell tokens and verifies only the binary name against a static allowlist. All trailing tokens are appended directly to the os/exec.Command invocation.
By supplying compressor-specific flags, an attacker with backup permissions can alter the behavior of the invoked compression utility. Specifically, when invoking zstd, an attacker can inject flags that switch the utility into pass-through decompression mode, specify an arbitrary input file from the container's mounted rootfs on the host, and write the output to a privileged host path like /etc/cron.d/. This creates an arbitrary file write (CWE-73) primitive as root, resulting in a complete container-to-host escape and arbitrary code execution.
The root cause of GHSA-fmc8-p6q7-75cc resides in the disconnect between how LXD validates the compression_algorithm parameter and how it constructs the command execution slice in compressFile(). When a user submits a backup request or configures an instance compression property, the input string is tokenized using shellquote.Split(value), which parses the string according to standard POSIX shell quoting rules.
In the validation logic, LXD evaluates only the first element of the resulting slice (fields[0]). It checks whether fields[0] matches an allowlisted binary name (bzip2, gzip, lz4, lzma, pigz, pzstd, pxz, tar2sqfs, xz, zstd) and confirms the binary exists in the system PATH via exec.LookPath(fields[0]). However, the validation function does not check the length of the fields slice, nor does it inspect or reject fields[1:]. This design was originally intended to let users pass harmless compression level flags (such as gzip -9), but it lacked any restrictions on which flags could be passed.
When the backup routine calls compressFile(), the function re-parses the compress string with shellquote.Split. It initializes the argument slice with args := []string{"-c"} to instruct the compressor to write to standard output, and then appends all remaining user-provided tokens via args = append(args, fields[1:]...). The command is then executed directly on the host via exec.Command(fields[0], args...) with cmd.Stdin attached to the uncompressed backup stream and cmd.Stdout attached to the backup target file.
Because most standard UNIX compression utilities process command-line flags sequentially from left to right, later flags override earlier ones. Although LXD prepends -c (write to standard output), passing -o <path> to zstd overrides the -c flag and directs output to <path>. Furthermore, passing -- <source_file> instructs zstd to ignore standard input (cmd.Stdin) and instead read directly from <source_file> on the filesystem. Combining -d (decompress), -f (force overwrite), and --pass-through (copy uncompressed data verbatim when input is not a valid zstd frame) transforms zstd into an arbitrary file copy utility running as host root.
Prior to the patch, LXD validated the compression algorithm by splitting the string and checking fields[0] against a slice of permitted binaries, while ignoring additional tokens:
// Vulnerable validation logic
fields, err := shellquote.Split(value)
if err != nil {
return err
}
if len(fields) == 0 {
return fmt.Errorf("No compression algorithm specified")
}
// Only fields[0] is checked; fields[1:] are ignored during validation
if !slices.Contains([]string{"bzip2", "gzip", "lz4", "lzma", "pigz", "pzstd", "pxz", "tar2sqfs", "xz", "zstd"}, fields[0]) {
return fmt.Errorf("Compression algorithm %q isn't currently supported", fields[0])
}
_, err = exec.LookPath(fields[0])In lxd/images.go, the compressFile function took the validated compress string, split it again, and passed all trailing elements directly to exec.Command without sandboxing or argument filtering. Canonical addressed this in commits such as e224d6f8230a2da7ed7e60ab2aee44c788f9dba8 and 0915bb88cbb0baed5ac1aef8e62079c57ac877da by modifying compressFile to accept the daemon's *sys.OS structure and wrapping the command execution in a dynamically generated AppArmor profile via apparmor.CompressWrapper:
-func compressFile(compress string, infile io.Reader, outfile io.Writer) error {
+func compressFile(sysOS *sys.OS, compress string, infile io.Reader, outfile io.Writer) error {
reproducible := []string{"gzip"}
- var cmd *exec.Cmd
// Parse the command.
fields, err := shellquote.Split(compress)
...
- cmd := exec.Command(fields[0], args...)
- cmd.Stdin = infile
- cmd.Stdout = outfile
- err := cmd.Run()
- if err != nil {
- return err
- }
+ cmd := exec.Command(fields[0], args...)
+ cmd.Stdin = infile
+ cmd.Stdout = outfile
+ // Wrap the compressor command in a restrictive AppArmor profile
+ cleanup, err := apparmor.CompressWrapper(sysOS, cmd, "", nil)
+ if err != nil {
+ return err
+ }
+ defer cleanup()
+ err = cmd.Run()
+ if err != nil {
+ return err
+ }The apparmor.CompressWrapper function (defined in lxd/apparmor/archive.go) generates a temporary AppArmor profile from compressProfileTpl and rewrites cmd to execute under aa-exec -p <profileName> <command> <args...>. The template enforces strict Mandatory Access Control (MAC) rules at the kernel level:
var compressProfileTpl = template.Must(template.New("compressProfile").Parse(`#include <tunables/global>
profile "{{.name}}" {
#include <abstractions/base>
{{range $index, $element := .allowedCommandPaths}}
{{$element}} mixr,
{{- end }}
{{- if .hasDstPath }}
{{ .dstPath }} rw,
{{- end }}
signal (receive) set=("term"),
{{- if .snap }}
# Snap-specific libraries
/snap/lxd/*/lib/**.so* mr,
{{- end }}
}
`))> [!WARNING]
> Fix Completeness Assessment: Rather than forbidding extra arguments in fields[1:] or validating flags against a strict allowlist, Canonical's remediation relies on AppArmor confinement to block unauthorized file access. While effective on standard Ubuntu systems where AppArmor is enabled and enforcing, CompressWrapper checks if !sysOS.AppArmorAvailable and returns a no-op cleanup function if AppArmor is absent or disabled. Consequently, LXD deployments running on Linux distributions or custom kernels without active AppArmor support remain vulnerable to this argument injection attack.
Exploiting GHSA-fmc8-p6q7-75cc requires network access to the LXD REST API (typically port 8443/tcp) and valid authentication credentials—such as a client TLS certificate or bearer token—that grant permission to access an instance and initiate a backup. The user does not need administrative privileges over the host or access to privileged container configurations; a standard unprivileged container within an isolated project is sufficient.
The attack proceeds in five distinct stages:
Payload Staging: The attacker creates a plain-text cron job file inside a target container using the LXD file upload API (POST /1.0/instances/{name}/files?path=/lxd-zstd-cron). Because zstd --pass-through only copies input verbatim when decompressed with -d, the staged file simply contains a standard /etc/cron.d/ line:
* * * * * root /bin/sh -c 'date >/incus-zstd-rce; id >>/incus-zstd-rce'Host Path Resolution: LXD mounts container filesystems at predictable locations on the host under /var/lib/lxd/storage-pools/<pool>/containers/<project>_<instance>/rootfs/ (or /var/snap/lxd/common/lxd/storage-pools/... for Snap installations). If the instance belongs to the default project, the directory name is simply <instance>. The staged file is therefore accessible to the host daemon at /var/lib/lxd/storage-pools/default/containers/<instance>/rootfs/lxd-zstd-cron.
Argument Injection via Backup API: The attacker sends a direct backup request to POST /1.0/instances/{name}/backups with Accept: application/octet-stream and the following JSON body:
{
"compression_algorithm": "zstd -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/lxd/storage-pools/default/containers/my-container/rootfs/lxd-zstd-cron",
"instance_only": true
}Arbitrary File Write: The LXD daemon constructs and executes zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- <source_path>. The zstd process reads the uncompressed cron specification from the container's rootfs path on the host and writes it directly to /etc/cron.d/incus-zstd-rce with root:root ownership and mode 0644. Note that the overall backup operation may return an HTTP 400/500 error after zstd exits because nothing was written to standard output, but the host file write has already succeeded.
Host Privilege Escalation: Within 60 seconds, the host's cron daemon parses /etc/cron.d/incus-zstd-rce and executes the injected shell command with full host root privileges.
The vulnerability carries a CVSS v3.1 base score of 9.9 (Critical) with the vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H. The critical rating reflects the Changed Scope (S:C) combined with Low Privileges Required (PR:L) and Low Attack Complexity (AC:L). An attacker who only possesses restricted API access to manage a single unprivileged container within a multi-tenant LXD deployment can completely compromise the underlying physical or virtual host.
Once arbitrary code execution as root on the host is achieved via /etc/cron.d/ injection (or alternative file write targets such as /root/.ssh/authorized_keys, /etc/ld.so.preload, or systemd unit directories), confidentiality, integrity, and availability of the entire host and all co-located containers and virtual machines are lost.
From a multi-tenancy perspective, LXD projects and RBAC policies are designed to isolate untrusted or semi-trusted users into distinct namespaces while sharing the same underlying LXD daemon and kernel. Because backup creation is a standard operational permission granted to project users, GHSA-fmc8-p6q7-75cc invalidates tenant isolation boundaries across all unpatched LXD and Incus deployments.
Administrators operating Canonical LXD or Linux Containers Incus must upgrade to a patched release immediately. In the github.com/canonical/lxd repository, the fix is incorporated starting from commit pseudo-version 0.0.0-20260630083100-c5184be310b1 and has been backported across supported stable branches (lxd-5.0, lxd-5.21). For Ubuntu systems using Snap packages, refresh the LXD snap channel using snap refresh lxd.
> [!IMPORTANT]
> Because Canonical's patch relies on apparmor.CompressWrapper to confine the compression binary rather than stripping command-line arguments in Go, AppArmor must be enabled and active on the host kernel. Verify that AppArmor is running and enforcing profiles by executing sudo aa-status. If AppArmor is disabled or unsupported on the host OS, the patch fails open and the system remains vulnerable.
In environments where immediate patching is not feasible, or on hosts without AppArmor support, apply the following mitigations:
lxd UNIX group or hold client TLS certificates/tokens registered with the LXD trust store. Revoke API access for untrusted multi-tenant users.:8443) is exposed through an API gateway or layer-7 reverse proxy that inspects JSON bodies, reject any requests to /1.0/instances/*/backups or /1.0/storage-pools/*/volumes/*/*/backups where the compression_algorithm field contains whitespace or hyphenated flags (-o, --pass-through, -d).auditd rules to alert on any unauthorized writes to cron directories or binary preloading configurations by compressor binaries.CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
github.com/canonical/lxd Canonical | < 0.0.0-20260630083100-c5184be310b1 | 0.0.0-20260630083100-c5184be310b1 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | GHSA-fmc8-p6q7-75cc (CVE-2026-48755) |
| Primary Weakness | CWE-88: Argument Injection |
| CVSS v3.1 Score | 9.9 Critical (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) |
| Affected Component | Canonical LXD (compressFile / backup compression parser) |
| Attack Vector | Authenticated REST API request (/1.0/instances/{name}/backups) |
| Security Impact | Container escape, arbitrary host file write, root RCE |
| Exploit Maturity | Public Proof-of-Concept (PoC) |
| CISA KEV Status | Not Listed |
The software constructs a command line with external input intended for a single argument or binary name, but does not neutralize delimiters or auxiliary command switches, allowing an attacker to inject arguments that modify the application's intended command line execution.
A critical path traversal vulnerability caused by improper input validation (CWE-20) exists in the direct image download handler of Canonical LXD and Linux Containers Incus. When an authenticated user requests an image import using the URL source type, the daemon reads the image fingerprint from an untrusted HTTP response header (`Incus-Image-Hash` or `LXD-Image-Hash`) without validating that it conforms to a SHA-256 format. This unvalidated string is concatenated directly into a filesystem path via `filepath.Join` prior to file creation and cryptographic verification, allowing directory traversal sequences to write arbitrary files on the host filesystem as root.
CVE-2026-9639 (GHSA-j93m-3j9p-m5m8) is a null pointer dereference vulnerability in Canonical LXD affecting versions 5.0.0 through 5.21.4 and 6.0 through 6.8. The flaw resides in the (*lxdBackend).CreateCustomVolumeFromBackup method within lxd/storage/backend_lxd.go. An authenticated attacker with the can_create_storage_volumes permission can upload a crafted custom volume backup tarball that omits the expires_at field on a snapshot entry, triggering an unhandled SIGSEGV panic and terminating the LXD daemon.
An operating system command injection vulnerability (CWE-78) exists in GeoNode versions prior to 5.1.0. The vulnerability occurs in the vector upload processing pipeline when handling user-supplied geospatial files. By supplying a crafted layer name containing shell metacharacters, an authenticated attacker with basic data upload privileges can execute arbitrary commands on the underlying server or container hosting the GeoNode application and Celery worker processes.
GeoNode versions prior to 5.0.3 contain an unrestricted file upload vulnerability (CWE-434) within the document and dataset ingestion architecture. Authenticated low-privileged users can bypass upload validation via direct REST API interactions, file extension masquerading, metadata mutation via PATCH requests, or malicious archive submissions. This allows attackers to store and serve dangerous file types, alter on-disk file references, or trigger path traversal and resource exhaustion during archive extraction.
CVE-2026-55649 is a stored Cross-Site Scripting (XSS) vulnerability in GeoNode versions prior to 5.0.3. The flaw exists because document abstract metadata fields are ingested via the metadata management pipeline and stored in the backend database without HTML sanitization or input neutralization. An authenticated attacker with privileges to create or edit document metadata can inject arbitrary HTML and JavaScript payloads into these fields. When another user, such as an administrator, views the affected document page or catalog listing, the unencoded payload is rendered into the Document Object Model (DOM) and executes within the victim's browser session.
CVE-2026-55622 (GHSA-qx75-2p3r-pwm5) is an improper access control vulnerability (CWE-284) in Canonical LXD and LXC Incus (tracked as GHSA-c9f5-j9c3-mhrg). In multi-tenant deployments utilizing project-based isolation, the REST API handler for instance creation via POST /1.0/instances verifies that the authenticated user has authorization in the destination project specified by the query parameter, but fails to verify whether the user holds read or view permissions for the source instance when executing a copy operation across projects. Consequently, an authenticated client restricted to a single project can clone instances or snapshots from unauthorized projects into their own project, resulting in complete disclosure of the target instance filesystem and embedded credentials.