From 377b4e5cb8a5e1724fafe2bedd4f91ea4ca102ef Mon Sep 17 00:00:00 2001 From: fengyl07 <1059732235@qq.com> Date: Mon, 6 Jul 2026 15:29:06 +0800 Subject: [PATCH] fix(read): block /proc/environ aliases and symlink bypass of credential file --- agent/tools/read/read.py | 47 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/agent/tools/read/read.py b/agent/tools/read/read.py index 69f5676c..b9738b4b 100644 --- a/agent/tools/read/read.py +++ b/agent/tools/read/read.py @@ -4,6 +4,7 @@ Supports text files, images (jpg, png, gif, webp), and PDF files """ import os +import re from typing import Dict, Any from pathlib import Path @@ -12,6 +13,12 @@ from agent.tools.utils.truncate import truncate_head, format_size, DEFAULT_MAX_L from common.utils import expand_path +# Paths whose CONTENT mirrors the process environment (and thus any secrets +# loaded from ~/.cow/.env). Reading them bypasses the env_config boundary. +# Matches /proc/self/environ, /proc/thread-self/environ and /proc//environ. +_PROC_ENVIRON_RE = re.compile(r"^/proc/(\d+|self|thread-self)/environ$") + + class Read(BaseTool): """Tool for reading file contents""" @@ -79,9 +86,9 @@ class Read(BaseTool): # Resolve path absolute_path = self._resolve_path(path) - # Security check: Prevent reading sensitive config files - env_config_path = expand_path("~/.cow/.env") - if os.path.abspath(absolute_path) == os.path.abspath(env_config_path): + # Security check: block credential files and their aliases. + # See issue #2913 (/proc/self/environ bypass) and #2863 (scope). + if self._is_credential_path(absolute_path): return ToolResult.fail( "Error: Access denied. API keys and credentials must be accessed through the env_config tool only." ) @@ -140,7 +147,39 @@ class Read(BaseTool): if os.path.isabs(path): return path return os.path.abspath(os.path.join(self.cwd, path)) - + + def _is_credential_path(self, absolute_path: str) -> bool: + """Return True if *absolute_path* points at protected credential data. + + Beyond the literal ~/.cow/.env file, this also blocks two real bypass + surfaces reported in issue #2913: + 1. /proc//environ — a second view of the + process environment that leaks secrets loaded from ~/.cow/.env. + 2. Symlinks resolving to ~/.cow/.env; the previous exact abspath + match kept the link target and could be bypassed. + + Scope is kept deliberately narrow (only the credential file and its + environ aliases) so this does NOT re-broaden the block that #2863 + intentionally narrowed to ~/.cow/.env. + """ + # Compare on both the normalized path and the symlink-resolved path, + # in POSIX form so the /proc regex matches regardless of os.sep. + candidates = set() + try: + candidates.add(os.path.normpath(absolute_path).replace(os.sep, "/")) + candidates.add(os.path.realpath(absolute_path).replace(os.sep, "/")) + except OSError: + candidates.add(absolute_path.replace(os.sep, "/")) + + # 1. /proc environ aliases (checked on raw and symlink-resolved forms). + for candidate in candidates: + if _PROC_ENVIRON_RE.match(candidate): + return True + + # 2. The credential file itself, following symlinks on both sides. + env_real = os.path.realpath(expand_path("~/.cow/.env")).replace(os.sep, "/") + return env_real in candidates + def _return_file_metadata(self, absolute_path: str, file_type: str, file_size: int) -> ToolResult: """ Return file metadata for non-readable files (video, audio, binary, etc.)