VYPR
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

1

Patches

1
77d1dda8303c

Merge commit from fork

https://github.com/microsoft/apmDaniel MeppielMay 11, 2026via ghsa
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

5

News mentions

0

No linked articles in our index yet.