VYPR
Medium severity4.4NVD Advisory· Published Mar 31, 2026· Updated Apr 13, 2026

CVE-2026-34450

CVE-2026-34450

Description

The Claude SDK for Python provides access to the Claude API from Python applications. From version 0.86.0 to before version 0.87.0, the local filesystem memory tool in the Anthropic Python SDK created memory files with mode 0o666, leaving them world-readable on systems with a standard umask and world-writable in environments with a permissive umask such as many Docker base images. A local attacker on a shared host could read persisted agent state, and in containerized deployments could modify memory files to influence subsequent model behavior. Both the synchronous and asynchronous memory tool implementations were affected. This issue has been patched in version 0.87.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
anthropicPyPI
>= 0.86.0, < 0.87.00.87.0

Affected products

1

Patches

1
715030ceb4d6

fix(memory): use restrictive file mode for memory files

1 file changed · +21 12
  • src/anthropic/lib/tools/_beta_builtin_memory_tool.py+21 12 modified
    @@ -42,6 +42,15 @@
     MAX_LINES = 999999
     LINE_NUMBER_WIDTH = len(str(MAX_LINES))
     
    +# Owner read/write only. Avoids 0o666 which, in environments with a permissive
    +# umask (e.g. Docker where umask is often 0o000), would make memory files
    +# world-readable or even world-writable.
    +_FILE_CREATE_MODE = 0o600
    +# The default mkdir mode is 0o777, but we want to be more restrictive for memory
    +# directories to avoid them being world-accessible in environments with permissive umasks
    +# (eg Docker)
    +_DIR_CREATE_MODE = 0o700
    +
     
     class BetaAbstractMemoryTool(BetaBuiltinFunctionTool):
         """Abstract base class for memory tool implementations.
    @@ -275,7 +284,7 @@ def _atomic_write_file(target_path: Path, content: str) -> None:
         data = content.encode("utf-8")
     
         try:
    -        fd = os.open(temp_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o666)
    +        fd = os.open(temp_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, _FILE_CREATE_MODE)
             try:
                 offset = 0
                 while offset < len(data):
    @@ -342,7 +351,7 @@ def __init__(self, base_path: str = "./memory"):
             super().__init__()
             self.base_path = Path(base_path)
             self.memory_root = self.base_path / "memories"
    -        self.memory_root.mkdir(parents=True, exist_ok=True)
    +        self.memory_root.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
     
         def _validate_path(self, path: str) -> Path:
             """Validate and resolve memory paths"""
    @@ -434,10 +443,10 @@ def collect_items(dir_path: Path, relative_path: str, depth: int) -> None:
         def create(self, command: BetaMemoryTool20250818CreateCommand) -> str:
             full_path = self._validate_path(command.path)
     
    -        full_path.parent.mkdir(parents=True, exist_ok=True)
    +        full_path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
     
             try:
    -            fd = os.open(full_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o666)
    +            fd = os.open(full_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, _FILE_CREATE_MODE)
                 try:
                     os.write(fd, command.file_text.encode("utf-8"))
                     os.fsync(fd)
    @@ -549,7 +558,7 @@ def rename(self, command: BetaMemoryTool20250818RenameCommand) -> str:
             if new_full_path.exists():
                 raise ToolError(f"The destination {command.new_path} already exists")
     
    -        new_full_path.parent.mkdir(parents=True, exist_ok=True)
    +        new_full_path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
     
             try:
                 old_full_path.rename(new_full_path)
    @@ -563,7 +572,7 @@ def clear_all_memory(self) -> str:
             """Override the base implementation to provide file system clearing."""
             if self.memory_root.exists():
                 shutil.rmtree(self.memory_root)
    -        self.memory_root.mkdir(parents=True, exist_ok=True)
    +        self.memory_root.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
             return "All memory cleared"
     
     
    @@ -576,7 +585,7 @@ async def _async_atomic_write_file(target_path: AsyncPath, content: str) -> None
         try:
     
             def write_replace_and_sync() -> None:
    -            fd = os.open(sync_temp_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o666)
    +            fd = os.open(sync_temp_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, _FILE_CREATE_MODE)
                 try:
                     offset = 0
                     while offset < len(data):
    @@ -624,7 +633,7 @@ def __init__(self, base_path: str = "./memory"):
     
         async def _ensure_memory_root(self) -> None:
             """Ensure the memory root directory exists"""
    -        await self.memory_root.mkdir(parents=True, exist_ok=True)
    +        await self.memory_root.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
     
         async def _validate_path(self, path: str) -> AsyncPath:
             """Validate and resolve memory paths"""
    @@ -724,13 +733,13 @@ async def create(self, command: BetaMemoryTool20250818CreateCommand) -> str:
             await self._ensure_memory_root()
             full_path = await self._validate_path(command.path)
     
    -        await full_path.parent.mkdir(parents=True, exist_ok=True)
    +        await full_path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
     
             try:
                 sync_full_path = Path(str(full_path))
     
                 def create_exclusive() -> None:
    -                fd = os.open(sync_full_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o666)
    +                fd = os.open(sync_full_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, _FILE_CREATE_MODE)
                     try:
                         os.write(fd, command.file_text.encode("utf-8"))
                         os.fsync(fd)
    @@ -848,7 +857,7 @@ async def rename(self, command: BetaMemoryTool20250818RenameCommand) -> str:
             if await new_full_path.exists():
                 raise ToolError(f"The destination {command.new_path} already exists")
     
    -        await new_full_path.parent.mkdir(parents=True, exist_ok=True)
    +        await new_full_path.parent.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
     
             try:
                 await old_full_path.rename(new_full_path)
    @@ -862,5 +871,5 @@ async def clear_all_memory(self) -> str:
             """Override the base implementation to provide file system clearing."""
             if await self.memory_root.exists():
                 await run_sync(shutil.rmtree, str(self.memory_root))
    -        await self.memory_root.mkdir(parents=True, exist_ok=True)
    +        await self.memory_root.mkdir(parents=True, exist_ok=True, mode=_DIR_CREATE_MODE)
             return "All memory cleared"
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.