Medium severity5.5GHSA Advisory· Published May 15, 2026· Updated May 15, 2026
CVE-2026-46383
CVE-2026-46383
Description
Microsoft APM is an open-source, community-driven dependency manager for AI agents. Prior to 0.13.0, Microsoft APM contains a Windows-specific archive extraction boundary failure in the legacy-bundle probe used by apm install <bundle> on supported Python 3.10 and 3.11 runtimes. When apm install is given a local .tar.gz that is not recognized as a plugin-format bundle, APM probes whether it is a legacy --format apm bundle. On Python versions earlier than 3.12, that probe extracts untrusted tar members with raw tar.extractall() without rejecting Windows absolute member names such as D:/.... This vulnerability is fixed in 0.13.0.
Affected products
1Patches
177d1dda8303cMerge commit from fork
3 files changed · +174 −5
src/apm_cli/bundle/local_bundle.py+12 −1 modified@@ -152,10 +152,21 @@ def _looks_like_legacy_apm_bundle(path: Path) -> bool: for member in tar.getmembers(): if member.issym() or member.islnk(): return False + name = member.name + if ( + name.startswith("/") + or PureWindowsPath(name).drive + or PureWindowsPath(name).is_absolute() + ): + return False + try: + validate_path_segments(name, context="tar member") + except PathTraversalError: + return False if sys.version_info >= (3, 12): tar.extractall(tmp, filter="data") else: - tar.extractall(tmp) # noqa: S202 + tar.extractall(tmp) # noqa: S202 -- validated above # Locate the inner directory (apm pack uses arcname=<bundle-name>) root = tmp children = [p for p in tmp.iterdir() if p.is_dir()]
src/apm_cli/bundle/unpacker.py+16 −4 modified@@ -5,10 +5,11 @@ import tarfile import tempfile from dataclasses import dataclass, field -from pathlib import Path +from pathlib import Path, PureWindowsPath from typing import Dict, List # noqa: F401, UP035 from ..deps.lockfile import LEGACY_LOCKFILE_NAME, LOCKFILE_NAME, LockFile +from ..utils.path_security import PathTraversalError, validate_path_segments @dataclass @@ -64,10 +65,21 @@ def unpack_bundle( with tarfile.open(bundle_path, "r:gz") as tar: # Security: prevent path traversal and special entries for member in tar.getmembers(): - if member.name.startswith("/") or ".." in member.name: - raise ValueError(f"Refusing to extract path-traversal entry: {member.name}") + name = member.name + if ( + name.startswith("/") + or PureWindowsPath(name).drive + or PureWindowsPath(name).is_absolute() + ): + raise ValueError(f"Refusing to extract path-traversal entry: {name}") + try: + validate_path_segments(name, context="tar member") + except PathTraversalError: + raise ValueError( + f"Refusing to extract path-traversal entry: {name}" + ) from None if member.issym() or member.islnk(): - raise ValueError(f"Refusing to extract symlink/hardlink: {member.name}") + raise ValueError(f"Refusing to extract symlink/hardlink: {name}") # filter="data" was added in Python 3.12; use it when available if sys.version_info >= (3, 12): tar.extractall(temp_dir, filter="data")
tests/unit/bundle/test_tar_windows_absolute_path.py+146 −0 added@@ -0,0 +1,146 @@ +"""Regression tests for Windows absolute path rejection in tar extraction. + +Verifies that _looks_like_legacy_apm_bundle() and the unpacker reject +tar members with Windows absolute paths (e.g., D:/...) before extraction. +""" + +from __future__ import annotations + +import io +import tarfile +from pathlib import Path + +import pytest + + +class TestLegacyBundleProbeRejectsWindowsAbsolutePaths: + """Verify _looks_like_legacy_apm_bundle rejects Windows absolute paths.""" + + def _make_tarball_with_members(self, tmp_path: Path, members: list[tuple[str, str]]) -> Path: + """Create a .tar.gz with the given (name, content) members.""" + tarball_path = tmp_path / "malicious.tar.gz" + with tarfile.open(tarball_path, "w:gz") as tar: + for name, content in members: + data = content.encode("utf-8") + info = tarfile.TarInfo(name=name) + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + return tarball_path + + def test_rejects_windows_drive_letter_path(self, tmp_path: Path) -> None: + from apm_cli.bundle.local_bundle import _looks_like_legacy_apm_bundle + + tarball = self._make_tarball_with_members( + tmp_path, + [ + ("bundle/apm.lock.yaml", "packages: []"), + ("D:/evil/payload.txt", "malicious content"), + ], + ) + + assert _looks_like_legacy_apm_bundle(tarball) is False + + def test_rejects_windows_unc_path(self, tmp_path: Path) -> None: + from apm_cli.bundle.local_bundle import _looks_like_legacy_apm_bundle + + tarball = self._make_tarball_with_members( + tmp_path, + [ + ("bundle/apm.lock.yaml", "packages: []"), + ("//server/share/payload.txt", "malicious content"), + ], + ) + + assert _looks_like_legacy_apm_bundle(tarball) is False + + def test_rejects_unix_absolute_path(self, tmp_path: Path) -> None: + from apm_cli.bundle.local_bundle import _looks_like_legacy_apm_bundle + + tarball = self._make_tarball_with_members( + tmp_path, + [ + ("bundle/apm.lock.yaml", "packages: []"), + ("/etc/passwd", "malicious content"), + ], + ) + + assert _looks_like_legacy_apm_bundle(tarball) is False + + def test_rejects_dot_dot_traversal(self, tmp_path: Path) -> None: + from apm_cli.bundle.local_bundle import _looks_like_legacy_apm_bundle + + tarball = self._make_tarball_with_members( + tmp_path, + [ + ("bundle/apm.lock.yaml", "packages: []"), + ("bundle/../../etc/passwd", "malicious content"), + ], + ) + + assert _looks_like_legacy_apm_bundle(tarball) is False + + def test_accepts_valid_legacy_bundle(self, tmp_path: Path) -> None: + from apm_cli.bundle.local_bundle import _looks_like_legacy_apm_bundle + + tarball = self._make_tarball_with_members( + tmp_path, + [ + ("bundle/apm.lock.yaml", "packages: []"), + ("bundle/README.md", "hello"), + ], + ) + + assert _looks_like_legacy_apm_bundle(tarball) is True + + def test_no_file_created_outside_temp(self, tmp_path: Path) -> None: + """Ensure no file is created at the Windows absolute path.""" + from apm_cli.bundle.local_bundle import _looks_like_legacy_apm_bundle + + escape_target = tmp_path / "outside" / "payload.txt" + + tarball = self._make_tarball_with_members( + tmp_path, + [ + ("bundle/apm.lock.yaml", "packages: []"), + (str(escape_target), "should not be written"), + ], + ) + + _looks_like_legacy_apm_bundle(tarball) + + assert not escape_target.exists() + + +class TestUnpackerRejectsWindowsAbsolutePaths: + """Verify unpacker rejects Windows absolute paths in tar members.""" + + def _make_tarball_with_members(self, tmp_path: Path, members: list[tuple[str, str]]) -> Path: + """Create a .tar.gz with the given (name, content) members.""" + tarball_path = tmp_path / "malicious.tar.gz" + with tarfile.open(tarball_path, "w:gz") as tar: + for name, content in members: + data = content.encode("utf-8") + info = tarfile.TarInfo(name=name) + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + return tarball_path + + def test_rejects_windows_drive_letter_in_unpack(self, tmp_path: Path) -> None: + from apm_cli.bundle.unpacker import unpack_bundle + + tarball = self._make_tarball_with_members( + tmp_path, + [ + ( + "bundle/apm.lock.yaml", + "packages: []\ndeployed_files:\n README.md: {hash: abc}", + ), + ("D:/evil/payload.txt", "malicious content"), + ], + ) + + output_dir = tmp_path / "output" + output_dir.mkdir() + + with pytest.raises(ValueError, match=r"path-traversal"): + unpack_bundle(tarball, output_dir)
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
5News mentions
0No linked articles in our index yet.