mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
Add _safe_resolve() helper that uses os.path.realpath() to canonicalize the joined path and verifies it stays within LOCAL_STORAGE_PATH. All six public methods (save, load, exists, delete, size, delete_dir_recursive) now validate the key before performing any I/O. This prevents absolute-path injection (e.g. key="/etc/passwd") and relative traversal (e.g. key="../../etc/passwd") from escaping the storage root directory. CWE-22
87 lines
2.5 KiB
Python
87 lines
2.5 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import aiofiles
|
|
import shutil
|
|
|
|
from ...core import app
|
|
|
|
from .. import provider
|
|
|
|
|
|
LOCAL_STORAGE_PATH = os.path.join('data', 'storage')
|
|
|
|
|
|
def _safe_resolve(base: str, key: str) -> str:
|
|
"""Resolve *key* under *base* and ensure the result stays inside *base*.
|
|
|
|
Raises ``ValueError`` if the resolved path escapes the storage root
|
|
(e.g. via absolute paths, ``..`` components, or symlinks).
|
|
"""
|
|
# os.path.realpath resolves symlinks and normalises the path.
|
|
resolved = os.path.realpath(os.path.join(base, key))
|
|
canonical_base = os.path.realpath(base)
|
|
# The resolved path must be *strictly* inside the base directory (or equal
|
|
# to it only for directory operations). We append os.sep so that a base of
|
|
# "/data/storage" does not match "/data/storage_evil".
|
|
if not (resolved == canonical_base or resolved.startswith(canonical_base + os.sep)):
|
|
raise ValueError(f'Path traversal detected: key {key!r} resolves outside storage root')
|
|
return resolved
|
|
|
|
|
|
class LocalStorageProvider(provider.StorageProvider):
|
|
def __init__(self, ap: app.Application):
|
|
super().__init__(ap)
|
|
if not os.path.exists(LOCAL_STORAGE_PATH):
|
|
os.makedirs(LOCAL_STORAGE_PATH)
|
|
|
|
async def save(
|
|
self,
|
|
key: str,
|
|
value: bytes,
|
|
):
|
|
resolved = _safe_resolve(LOCAL_STORAGE_PATH, key)
|
|
parent = os.path.dirname(resolved)
|
|
if not os.path.exists(parent):
|
|
os.makedirs(parent)
|
|
async with aiofiles.open(resolved, 'wb') as f:
|
|
await f.write(value)
|
|
|
|
async def load(
|
|
self,
|
|
key: str,
|
|
) -> bytes:
|
|
resolved = _safe_resolve(LOCAL_STORAGE_PATH, key)
|
|
async with aiofiles.open(resolved, 'rb') as f:
|
|
return await f.read()
|
|
|
|
async def exists(
|
|
self,
|
|
key: str,
|
|
) -> bool:
|
|
resolved = _safe_resolve(LOCAL_STORAGE_PATH, key)
|
|
return os.path.exists(resolved)
|
|
|
|
async def delete(
|
|
self,
|
|
key: str,
|
|
):
|
|
resolved = _safe_resolve(LOCAL_STORAGE_PATH, key)
|
|
os.remove(resolved)
|
|
|
|
async def size(
|
|
self,
|
|
key: str,
|
|
) -> int:
|
|
resolved = _safe_resolve(LOCAL_STORAGE_PATH, key)
|
|
return os.path.getsize(resolved)
|
|
|
|
async def delete_dir_recursive(
|
|
self,
|
|
dir_path: str,
|
|
):
|
|
resolved = _safe_resolve(LOCAL_STORAGE_PATH, dir_path)
|
|
# 直接删除整个目录
|
|
if os.path.exists(resolved):
|
|
shutil.rmtree(resolved)
|