CVE-2026-103929: Mise Mise Permits Untrusted Local Configuration Vulnerability

HIGH
Advisory Identifier: GHSA-g74g-rg72-j2p3
CVSS 7.8
CWE-78 CWE-284
Published
Advisory Summary

CVE-2026-103929 is a HIGH (CVSS 7.8) mise permits untrusted local configuration vulnerability (CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')) affecting Mise libraries in the crates.io ecosystem. The issue involves mise permits untrusted local configuration to override default shell arguments. Remediation is available by upgrading affected packages to version 2026.7.14.

CVE-2026-103929 at a Glance

CVE Identifier CVE-2026-103929
GHSA Identifier GHSA-g74g-rg72-j2p3
Severity HIGH
CVSS v3.1 Base Score 7.8
CWE Classification CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Affected Product Mise
Package Ecosystem crates.io
Published Date
Last Updated

Security Impact

CVSS: 7.8

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H).

Attack Vector Local (AV:L)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction Required (UI:R)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-103929

To remediate this vulnerability, upgrade all affected Mise dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

mise fixed: 2026.7.14
Upgrade mise to 2026.7.14
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for mise

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-103929, inspect your project dependencies for Mise:

inspect dependencies for mise

Analyze the terminal output to locate any version of Mise in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 2026.7.14), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection'), where the product constructs all or part of an os command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended os command when it is sent to a downstream component. In Mise, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## Summary Untrusted local .mise.toml [settings] can set the shell interpreter args (unix/windows default_inline / default_file shell_args), yielding arbitrary command execution, because those command-interpreter settings were not added to the global_only 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 global_only denylist: settings marked global_only are stripped from non-global config. The maintainers marked the command-execution fields they knew about (the three forge credential_command 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 global_only denylist and the gap. In settings.toml, the three credential_command sinks are global_only (forgejo.credential_command around line 775, github.credential_command around 828, gitlab.credential_command around 990). The four shell-args settings do not carry global_only: - unix_default_file_shell_args (settings.toml around 2920) - unix_default_inline_shell_args (around 2926) - windows_default_file_shell_args (around 3004) - windows_default_inline_shell_args (around 3010) The strip logic only removes global_only keys. src/config/settings.rs strip_local_only_settings (line 409) filters by meta.global_only (line 416: `filter_map(|(key, meta)| meta.global_only.then_some(*key))`), so any field not marked global_only survives from a local untrusted config. The settings preload loads every config path including the local project file (all_settings_files, line 778), and in default (non-safe) mode there is no trust check (safe_mode defaults false, line 1141). The sink. src/config/settings.rs default_inline_shell (line 1032) reads self.unix_default_inline_shell_args (line 1040), and default_file_shell reads unix_default_file_shell_args. Settings::get returns the merged global-plus-local value minus global_only, 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 (tera1_exec around 1358, and around 1470) builds shell[1..] + [command] with program = shell[0]. An attacker unix_default_inline_shell_args = "sh -c 'PAYLOAD'" yields sh -c 'PAYLOAD' <command>. - postinstall runner: src/backend/mod.rs around 2405 calls cmd_body_args(shell_args, body); src/cmd.rs cmd_body_args (non-Windows, around 520) returns program flags... body, the same demotion. - task execution: src/task/task_executor.rs (around 916, 939, 993) uses task.shell()?.unwrap_or(default_inline_shell()/default_file_shell()), 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 default_inline_shell(). ## 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] unix_default_inline_shell_args = "sh -c 'id > /tmp/mise_pwned'" ``` 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/mise_pwned' <realcommand>`, so the payload runs. ## Verification Source-verified at HEAD: the four shell-args settings lack global_only while the credential_command sinks have it, strip_local_only_settings removes only global_only keys, default_inline_shell 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 *_default_*_shell_args settings global_only = true (the same treatment as credential_command), 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 sanitize_install_version_name only inside the HTTP backend (src/backend/http.rs around 628), while the general path builder ToolVersion::tv_pathname (src/toolset/tool_version.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 strip_local_only_settings (confirming it strips only global_only), the settings.toml entries (confirming the four shell-args settings lack global_only while credential_command has it), default_inline_shell (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.

CVE-2026-103929 Affected Mise Versions

1 component(s)
Fixed Versions:
2026.7.14
Product / Package Ecosystem Affected Versions Fixed Version
mise
crates.io
< 2026.7.14
2026.7.14

References

Frequently Asked Questions

What is CVE-2026-103929?
CVE-2026-103929 is a security vulnerability in Mise involving mise permits untrusted local configuration to override default shell arguments under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
Which Mise versions are affected?
Affected versions include releases prior to 2026.7.14 across affected packages: mise.
What version fixes CVE-2026-103929?
Upgrading to 2026.7.14 (or the latest patched release for your respective branch) resolves the vulnerability.
Is mise affected?
Yes, mise is affected in releases prior to 2026.7.14. Upgrade your dependency to 2026.7.14 or later.
What is the CVSS score of CVE-2026-103929?
CVE-2026-103929 has a CVSS v3.1 base score of 7.8 (HIGH) with vector string CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H.
What CWE is associated with CVE-2026-103929?
CVE-2026-103929 is classified under CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.).
How can I check crates.io dependencies for CVE-2026-103929?
Run the dependency tree command: 'inspect dependencies for mise'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-g74g-rg72-j2p3",
  "modified" : "2026-10-15T20:16:09Z",
  "published" : "2026-10-15T20:16:09Z",
  "aliases" : [ "CVE-2026-103929" ],
  "summary" : "mise permits untrusted local configuration to override default shell arguments",
  "details" : "## Summary\n\nUntrusted local .mise.toml [settings] can set the shell interpreter args (unix/windows default_inline / default_file shell_args), yielding arbitrary command execution, because those command-interpreter settings were not added to the global_only 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.\n\n## The defect\n\nCVE-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 global_only denylist: settings marked global_only are stripped from non-global config. The maintainers marked the command-execution fields they knew about (the three forge credential_command 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.\n\nThe global_only denylist and the gap. In settings.toml, the three credential_command sinks are global_only (forgejo.credential_command around line 775, github.credential_command around 828, gitlab.credential_command around 990). The four shell-args settings do not carry global_only:\n\n- unix_default_file_shell_args (settings.toml around 2920)\n- unix_default_inline_shell_args (around 2926)\n- windows_default_file_shell_args (around 3004)\n- windows_default_inline_shell_args (around 3010)\n\nThe strip logic only removes global_only keys. src/config/settings.rs strip_local_only_settings (line 409) filters by meta.global_only (line 416: `filter_map(|(key, meta)| meta.global_only.then_some(*key))`), so any field not marked global_only survives from a local untrusted config. The settings preload loads every config path including the local project file (all_settings_files, line 778), and in default (non-safe) mode there is no trust check (safe_mode defaults false, line 1141).\n\nThe sink. src/config/settings.rs default_inline_shell (line 1032) reads self.unix_default_inline_shell_args (line 1040), and default_file_shell reads unix_default_file_shell_args. Settings::get returns the merged global-plus-local value minus global_only, so the untrusted project value overrides the built-in default (sh -c -o errexit).\n\nInjection semantics. The real command is appended after the attacker's args, so PAYLOAD runs and the real command is demoted to $0:\n\n- tera inline exec builtin: src/tera.rs (tera1_exec around 1358, and around 1470) builds shell[1..] + [command] with program = shell[0]. An attacker unix_default_inline_shell_args = \"sh -c 'PAYLOAD'\" yields sh -c 'PAYLOAD' <command>.\n- postinstall runner: src/backend/mod.rs around 2405 calls cmd_body_args(shell_args, body); src/cmd.rs cmd_body_args (non-Windows, around 520) returns program flags... body, the same demotion.\n- task execution: src/task/task_executor.rs (around 916, 939, 993) uses task.shell()?.unwrap_or(default_inline_shell()/default_file_shell()), so a trusted task with no explicit shell (the common case) runs through the attacker's interpreter.\n- hooks: src/hooks.rs (around 540, 603) also fall back to default_inline_shell().\n\n## Attacker model and reachability\n\nAn 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.\n\nProof of concept. In an untrusted repo, .mise.toml:\n\n```toml\n[settings]\nunix_default_inline_shell_args = \"sh -c 'id > /tmp/mise_pwned'\"\n```\n\nThe 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/mise_pwned' <realcommand>`, so the payload runs.\n\n## Verification\n\nSource-verified at HEAD: the four shell-args settings lack global_only while the credential_command sinks have it, strip_local_only_settings removes only global_only keys, default_inline_shell 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.\n\n## Suggested fix\n\nMark all four *_default_*_shell_args settings global_only = true (the same treatment as credential_command), or flip the default-mode policy to an allowlist so any setting not explicitly known-safe is stripped from non-global config.\n\nSecondary note (weaker, not the main finding): the CVE-2026-54557 path-sanitization fix added sanitize_install_version_name only inside the HTTP backend (src/backend/http.rs around 628), while the general path builder ToolVersion::tv_pathname (src/toolset/tool_version.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.\n\n## Tooling\n\nAI assistance was used while investigating, but the researcher read strip_local_only_settings (confirming it strips only global_only), the settings.toml entries (confirming the four shell-args settings lack global_only while credential_command has it), default_inline_shell (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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "crates.io",
      "name" : "mise"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "2026.7.14"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 2026.7.13"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/jdx/mise/security/advisories/GHSA-g74g-rg72-j2p3"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-103929"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/jdx/mise/pull/11293"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/jdx/mise/commit/c6cd7a9a2ca580918562566126646e5d04317895"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/jdx/mise"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/jdx/mise/releases/tag/v2026.7.14"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-284", "CWE-78" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-15T20:16:09Z",
    "nvd_published_at" : "2026-10-14T16:17:30Z"
  }
}