Summary
Untrusted local .mise.toml [settings] can set the shell interpreter args (unix/windows defaultinline / defaultfile shellargs), yielding arbitrary command execution, because those command-interpreter settings were not added to the globalonly denylist the CVE-2026-55448 fix relies on. Code execution from an untrusted repository with no trust prompt for the malicious file, on par with the patched CVE. Confirmed at HEAD 7160170206782fdd3ef287c8302369e32152fb71.
The defect
CVE-2026-55448 and CVE-2026-35533 fixed the fact that a local project [settings] table is harvested by the settings preloader independent of the trust store (an untrusted .mise.toml sets settings without the user ever trusting it). The remediation is a per-field globalonly denylist: settings marked globalonly are stripped from non-global config. The maintainers marked the command-execution fields they knew about (the three forge credentialcommand values) but did not mark the four shell-interpreter-args settings, which are also command-execution settings because they choose the interpreter for every command mise spawns.
The globalonly denylist and the gap. In settings.toml, the three credentialcommand sinks are globalonly (forgejo.credentialcommand around line 775, github.credentialcommand around 828, gitlab.credentialcommand around 990). The four shell-args settings do not carry globalonly:
- unixdefaultfileshellargs (settings.toml around 2920) - unixdefaultinlineshellargs (around 2926) - windowsdefaultfileshellargs (around 3004) - windowsdefaultinlineshellargs (around 3010)
The strip logic only removes globalonly keys. src/config/settings.rs striplocalonlysettings (line 409) filters by meta.globalonly (line 416: filtermap(|(key, meta)| meta.globalonly.thensome(key))), so any field not marked globalonly survives from a local untrusted config. The settings preload loads every config path including the local project file (allsettingsfiles, line 778), and in default (non-safe) mode there is no trust check (safemode defaults false, line 1141).
The sink. src/config/settings.rs defaultinlineshell (line 1032) reads self.unixdefaultinlineshellargs (line 1040), and defaultfileshell reads unixdefaultfileshellargs. Settings::get returns the merged global-plus-local value minus globalonly, so the untrusted project value overrides the built-in default (sh -c -o errexit).
Injection semantics. The real command is appended after the attacker's args, so PAYLOAD runs and the real command is demoted to $0:
- tera inline exec builtin: src/tera.rs (tera1exec around 1358, and around 1470) builds shell[1..] + [command] with program = shell[0]. An attacker unixdefaultinlineshellargs = "sh -c 'PAYLOAD'" yields sh -c 'PAYLOAD' <command>. - postinstall runner: src/backend/mod.rs around 2405 calls cmdbodyargs(shellargs, body); src/cmd.rs cmdbodyargs (non-Windows, around 520) returns program flags... body, the same demotion. - task execution: src/task/taskexecutor.rs (around 916, 939, 993) uses task.shell()?.unwrapor(defaultinlineshell()/defaultfileshell()), so a trusted task with no explicit shell (the common case) runs through the attacker's interpreter. - hooks: src/hooks.rs (around 540, 603) also fall back to defaultinlineshell().
Attacker model and reachability
An untrusted repository the developer clones and cd's into. The attacker's .mise.toml sits in the working directory or any ancestor and is never trusted by the victim; its [settings] table is harvested by the preloader independent of the trust store, exactly as the two prior CVEs established. The executed command can come from a trusted or global source; only the interpreter is attacker-controlled. Any of these, run while the untrusted .mise.toml is in an ancestor directory, fires it: mise run <task> for a task defined in the global config or a trusted parent repo (inline run, no explicit shell); mise install of any tool with a postinstall directive; any tera template that calls exec(...) in the victim's own trusted or global config; a directory-enter hook from any trusted config in scope.
Proof of concept. In an untrusted repo, .mise.toml:
toml [settings] unixdefaultinlineshellargs = "sh -c 'id > /tmp/misepwned'"
The victim cd's in (does not trust the file) and later runs any global or trusted-parent task with no explicit shell, or mise install of a tool with a postinstall, or a config that renders {{ exec(...) }}. mise builds sh -c 'id > /tmp/misepwned' <realcommand>, so the payload runs.
Verification
Source-verified at HEAD: the four shell-args settings lack globalonly while the credentialcommand sinks have it, striplocalonlysettings removes only globalonly keys, defaultinlineshell reads the setting, and the exec/postinstall/task paths append the real command after the attacker args. The load path is the same one two shipped CVEs already proved reachable. I did not run mise end to end.
Suggested fix
Mark all four defaultshellargs settings globalonly = true (the same treatment as credentialcommand), or flip the default-mode policy to an allowlist so any setting not explicitly known-safe is stripped from non-global config.
Secondary note (weaker, not the main finding): the CVE-2026-54557 path-sanitization fix added sanitizeinstallversionname only inside the HTTP backend (src/backend/http.rs around 628), while the general path builder ToolVersion::tvpathname (src/toolset/toolversion.rs around 292) still only replaces ':' and '/' and does not neutralize '..' or backslash. Impact is narrow (one level up, stays in the mise data dir on Unix), so this is a hardening note, not a primary finding.
Tooling
AI assistance was used while investigating, but the researcher read striplocalonlysettings (confirming it strips only globalonly), the settings.toml entries (confirming the four shell-args settings lack globalonly while credentialcommand has it), defaultinlineshell (confirming it reads the setting), and the exec/postinstall/task invocation sites (confirming the real command is appended after the attacker args) themself at HEAD, so the incomplete fix is source-verified.
mise manages dev tools like node, python, cmake, and terraform. Prior to 2026.7.1, release tar archives record mise/bin/mise with user and group ID 1001 and packaging/standalone/install.envsubst extracts and moves it without normalizing ownership, allowing a local user with those IDs to replace a root-installed executable, especially when MISEINSTALLPATH targets a shared location such as /usr/local/bin. This issue is fixed in version 2026.7.1.