mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
fix(read): block /proc/environ aliases and symlink bypass of credential file
This commit is contained in:
@@ -4,6 +4,7 @@ Supports text files, images (jpg, png, gif, webp), and PDF files
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
from pathlib import Path
|
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
|
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/<pid>/environ.
|
||||||
|
_PROC_ENVIRON_RE = re.compile(r"^/proc/(\d+|self|thread-self)/environ$")
|
||||||
|
|
||||||
|
|
||||||
class Read(BaseTool):
|
class Read(BaseTool):
|
||||||
"""Tool for reading file contents"""
|
"""Tool for reading file contents"""
|
||||||
|
|
||||||
@@ -79,9 +86,9 @@ class Read(BaseTool):
|
|||||||
# Resolve path
|
# Resolve path
|
||||||
absolute_path = self._resolve_path(path)
|
absolute_path = self._resolve_path(path)
|
||||||
|
|
||||||
# Security check: Prevent reading sensitive config files
|
# Security check: block credential files and their aliases.
|
||||||
env_config_path = expand_path("~/.cow/.env")
|
# See issue #2913 (/proc/self/environ bypass) and #2863 (scope).
|
||||||
if os.path.abspath(absolute_path) == os.path.abspath(env_config_path):
|
if self._is_credential_path(absolute_path):
|
||||||
return ToolResult.fail(
|
return ToolResult.fail(
|
||||||
"Error: Access denied. API keys and credentials must be accessed through the env_config tool only."
|
"Error: Access denied. API keys and credentials must be accessed through the env_config tool only."
|
||||||
)
|
)
|
||||||
@@ -141,6 +148,38 @@ class Read(BaseTool):
|
|||||||
return path
|
return path
|
||||||
return os.path.abspath(os.path.join(self.cwd, 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/<pid|self|thread-self>/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:
|
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.)
|
Return file metadata for non-readable files (video, audio, binary, etc.)
|
||||||
|
|||||||
Reference in New Issue
Block a user