修复关键安全漏洞(Bug #19和Bug #13)

修复的Bug:
- Bug #19: 路径遍历漏洞
- Bug #13: 浏览器上下文竞态条件

主要改进:
1. 路径遍历防护:
   - /screenshots/<filename> 端点添加is_safe_path()验证
   - /static/<path:filename> 端点添加is_safe_path()验证
   - 防止攻击者通过../等序列访问系统文件

2. 浏览器资源并发保护:
   - PlaywrightAutomation类添加_lock线程锁
   - get_iframe_safe()方法使用锁保护main_page访问
   - close()方法使用锁保护资源释放
   - _cleanup_on_exit()使用非阻塞锁避免退出死锁
   - 解决TOCTOU(Time-of-Check-Time-of-Use)竞态条件

影响:
- 防止路径遍历攻击,保护系统文件安全
- 防止多线程环境下的浏览器资源竞争
- 提升系统安全性和稳定性

受影响文件:
- app.py (路径验证)
- playwright_automation.py (线程锁)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-11 14:12:01 +08:00
parent 114a4107bb
commit 6a905909d9
2 changed files with 149 additions and 57 deletions

81
app.py
View File

@@ -98,11 +98,17 @@ user_accounts = {} # {user_id: {account_id: Account对象}}
active_tasks = {} # {account_id: Thread对象}
task_status = {} # {account_id: {"user_id": x, "username": y, "status": "排队中/运行中", "detail_status": "具体状态", "browse_type": z, "start_time": t, "source": s, "progress": {...}, "is_vip": bool}}
# 线程安全锁 - 修复Bug #12
active_tasks_lock = threading.Lock() # 保护 active_tasks 字典
task_status_lock = threading.Lock() # 保护 task_status 字典
user_accounts_lock = threading.Lock() # 保护 user_accounts 字典
# VIP优先级队列
vip_task_queue = [] # VIP用户任务队列
normal_task_queue = [] # 普通用户任务队列
task_queue_lock = threading.Lock()
log_cache = {} # {user_id: [logs]} 每个用户独立的日志缓存
log_cache_lock = threading.Lock() # 保护 log_cache 字典
log_cache_total_count = 0 # 全局日志总数,防止无限增长
# 日志缓存限制
@@ -214,6 +220,73 @@ class Account:
return result
# ========== 线程安全辅助函数 - 修复Bug #12 ==========
def safe_set_task(account_id, thread):
"""线程安全地设置任务"""
with active_tasks_lock:
active_tasks[account_id] = thread
def safe_get_task(account_id):
"""线程安全地获取任务"""
with active_tasks_lock:
return active_tasks.get(account_id)
def safe_remove_task(account_id):
"""线程安全地移除任务"""
with active_tasks_lock:
return active_tasks.pop(account_id, None)
def safe_set_task_status(account_id, status_dict):
"""线程安全地设置任务状态"""
with task_status_lock:
task_status[account_id] = status_dict
def safe_update_task_status(account_id, updates):
"""线程安全地更新任务状态"""
with task_status_lock:
if account_id in task_status:
task_status[account_id].update(updates)
def safe_get_task_status(account_id):
"""线程安全地获取任务状态"""
with task_status_lock:
return task_status.get(account_id, {}).copy()
def safe_remove_task_status(account_id):
"""线程安全地移除任务状态"""
with task_status_lock:
return task_status.pop(account_id, None)
def safe_get_all_task_status():
"""线程安全地获取所有任务状态"""
with task_status_lock:
return {k: v.copy() for k, v in task_status.items()}
def safe_add_log(user_id, log_entry):
"""线程安全地添加日志"""
global log_cache_total_count
with log_cache_lock:
if user_id not in log_cache:
log_cache[user_id] = []
# 限制每个用户的日志数量
if len(log_cache[user_id]) >= MAX_LOGS_PER_USER:
log_cache[user_id].pop(0)
log_cache_total_count = max(0, log_cache_total_count - 1)
log_cache[user_id].append(log_entry)
log_cache_total_count += 1
# 全局日志总数限制
if log_cache_total_count > MAX_TOTAL_LOGS:
# 删除最早用户的最早日志
for uid in list(log_cache.keys()):
if log_cache[uid]:
log_cache[uid].pop(0)
log_cache_total_count -= 1
break
@login_manager.user_loader
def load_user(user_id):
"""Flask-Login 用户加载"""
@@ -2078,6 +2151,10 @@ def serve_screenshot(filename):
if not filename.startswith(username_prefix + '_'):
return jsonify({"error": "无权访问"}), 403
# 防止路径遍历攻击 (Bug #19 fix)
if not is_safe_path(SCREENSHOTS_DIR, filename):
return jsonify({"error": "非法路径"}), 403
return send_from_directory(SCREENSHOTS_DIR, filename)
@@ -2177,6 +2254,10 @@ def handle_disconnect():
@app.route('/static/<path:filename>')
def serve_static(filename):
"""提供静态文件访问"""
# 防止路径遍历攻击 (Bug #19 fix)
if not is_safe_path('static', filename):
return jsonify({"error": "非法路径"}), 403
response = send_from_directory('static', filename)
# 禁用缓存,强制浏览器每次都重新加载
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'

View File

@@ -149,6 +149,7 @@ class PlaywrightAutomation:
self.page: Optional[Page] = None
self.main_page: Optional[Page] = None
self._closed = False # 防止重复关闭
self._lock = threading.Lock() # Bug #13 fix: 保护浏览器资源访问
# 注册退出清理函数,确保进程异常退出时也能关闭浏览器
atexit.register(self._cleanup_on_exit)
@@ -434,6 +435,8 @@ class PlaywrightAutomation:
"""
for attempt in range(max_retries if retry else 1):
try:
# Bug #13 fix: 使用锁保护main_page访问
with self._lock:
# 先检查main_page是否有效
if not self.main_page:
self.log("⚠ main_page无效")
@@ -1327,6 +1330,8 @@ class PlaywrightAutomation:
def close(self):
"""完全关闭浏览器进程(每个账号独立)并确保资源释放"""
# Bug #13 fix: 使用锁保护close操作
with self._lock:
# 防止重复关闭
if self._closed:
return
@@ -1383,6 +1388,9 @@ class PlaywrightAutomation:
def _cleanup_on_exit(self):
"""进程退出时的清理函数由atexit调用"""
# Bug #13 fix: 尝试获取锁,但不阻塞(避免退出时死锁)
acquired = self._lock.acquire(blocking=False)
try:
if not self._closed:
try:
# 静默关闭,避免在退出时产生过多日志
@@ -1395,6 +1403,9 @@ class PlaywrightAutomation:
self._closed = True
except:
pass # 退出时忽略所有错误
finally:
if acquired:
self._lock.release()
def __enter__(self):
"""Context manager支持 - 进入"""