GHSA-g74g-rg72-j2p3: OS Command Injection
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.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
rust/miseto a version that resolves this vulnerability.Fixed in 2026.7.14 - Configuration
Mark all four default shell-argument settings as global_only so they are stripped from non-global configuration. Alternatively, change the default-mode policy to an allowlist that strips any setting not explicitly known-safe from non-global configuration.
mise settings unix_default_inline_shell_args, unix_default_file_shell_args, windows_default_inline_shell_args, windows_default_file_shell_args = global_only = true
Event History
Frequently Asked Questions
Who is exposed to this issue?
Users who run mise in or against an untrusted repository containing a local .mise.toml file are exposed. The local [settings] table is processed independently of the trust store.
Does exploiting this require the repository configuration to be trusted?
No. A malicious local .mise.toml can set the affected settings without a trust prompt for that file, allowing code execution from an untrusted repository.
Which configuration settings should be reviewed?
Review untrusted local .mise.toml files for [settings] entries controlling shell interpreter arguments: unix or windows default_inline and default_file shell_args. These settings can select the interpreter used for commands mise spawns.
How can I identify potentially affected repositories?
Search repositories used with mise for local .mise.toml files containing a [settings] table and the affected shell-argument settings. Such settings were not included in the global_only denylist that strips protected settings from non-global configuration.