修复关键安全漏洞(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:
81
app.py
81
app.py
@@ -98,11 +98,17 @@ user_accounts = {} # {user_id: {account_id: Account对象}}
|
|||||||
active_tasks = {} # {account_id: Thread对象}
|
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}}
|
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优先级队列
|
||||||
vip_task_queue = [] # VIP用户任务队列
|
vip_task_queue = [] # VIP用户任务队列
|
||||||
normal_task_queue = [] # 普通用户任务队列
|
normal_task_queue = [] # 普通用户任务队列
|
||||||
task_queue_lock = threading.Lock()
|
task_queue_lock = threading.Lock()
|
||||||
log_cache = {} # {user_id: [logs]} 每个用户独立的日志缓存
|
log_cache = {} # {user_id: [logs]} 每个用户独立的日志缓存
|
||||||
|
log_cache_lock = threading.Lock() # 保护 log_cache 字典
|
||||||
log_cache_total_count = 0 # 全局日志总数,防止无限增长
|
log_cache_total_count = 0 # 全局日志总数,防止无限增长
|
||||||
|
|
||||||
# 日志缓存限制
|
# 日志缓存限制
|
||||||
@@ -214,6 +220,73 @@ class Account:
|
|||||||
return result
|
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
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
"""Flask-Login 用户加载"""
|
"""Flask-Login 用户加载"""
|
||||||
@@ -2078,6 +2151,10 @@ def serve_screenshot(filename):
|
|||||||
if not filename.startswith(username_prefix + '_'):
|
if not filename.startswith(username_prefix + '_'):
|
||||||
return jsonify({"error": "无权访问"}), 403
|
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)
|
return send_from_directory(SCREENSHOTS_DIR, filename)
|
||||||
|
|
||||||
|
|
||||||
@@ -2177,6 +2254,10 @@ def handle_disconnect():
|
|||||||
@app.route('/static/<path:filename>')
|
@app.route('/static/<path:filename>')
|
||||||
def serve_static(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 = send_from_directory('static', filename)
|
||||||
# 禁用缓存,强制浏览器每次都重新加载
|
# 禁用缓存,强制浏览器每次都重新加载
|
||||||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ class PlaywrightAutomation:
|
|||||||
self.page: Optional[Page] = None
|
self.page: Optional[Page] = None
|
||||||
self.main_page: Optional[Page] = None
|
self.main_page: Optional[Page] = None
|
||||||
self._closed = False # 防止重复关闭
|
self._closed = False # 防止重复关闭
|
||||||
|
self._lock = threading.Lock() # Bug #13 fix: 保护浏览器资源访问
|
||||||
|
|
||||||
# 注册退出清理函数,确保进程异常退出时也能关闭浏览器
|
# 注册退出清理函数,确保进程异常退出时也能关闭浏览器
|
||||||
atexit.register(self._cleanup_on_exit)
|
atexit.register(self._cleanup_on_exit)
|
||||||
@@ -434,14 +435,16 @@ class PlaywrightAutomation:
|
|||||||
"""
|
"""
|
||||||
for attempt in range(max_retries if retry else 1):
|
for attempt in range(max_retries if retry else 1):
|
||||||
try:
|
try:
|
||||||
# 先检查main_page是否有效
|
# Bug #13 fix: 使用锁保护main_page访问
|
||||||
if not self.main_page:
|
with self._lock:
|
||||||
self.log("⚠ main_page无效")
|
# 先检查main_page是否有效
|
||||||
return None
|
if not self.main_page:
|
||||||
|
self.log("⚠ main_page无效")
|
||||||
|
return None
|
||||||
|
|
||||||
iframe = self.main_page.frame('mainframe')
|
iframe = self.main_page.frame('mainframe')
|
||||||
if iframe:
|
if iframe:
|
||||||
return iframe
|
return iframe
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
if self.is_context_error(error_msg):
|
if self.is_context_error(error_msg):
|
||||||
@@ -1327,49 +1330,51 @@ class PlaywrightAutomation:
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""完全关闭浏览器进程(每个账号独立)并确保资源释放"""
|
"""完全关闭浏览器进程(每个账号独立)并确保资源释放"""
|
||||||
# 防止重复关闭
|
# Bug #13 fix: 使用锁保护close操作
|
||||||
if self._closed:
|
with self._lock:
|
||||||
return
|
# 防止重复关闭
|
||||||
self._closed = True
|
if self._closed:
|
||||||
|
return
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# 第一步:关闭上下文
|
# 第一步:关闭上下文
|
||||||
if self.context:
|
if self.context:
|
||||||
try:
|
try:
|
||||||
self.context.close()
|
self.context.close()
|
||||||
# self.log("上下文已关闭") # 精简日志
|
# self.log("上下文已关闭") # 精简日志
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"关闭上下文时出错: {str(e)}"
|
error_msg = f"关闭上下文时出错: {str(e)}"
|
||||||
self.log(error_msg)
|
self.log(error_msg)
|
||||||
errors.append(error_msg)
|
errors.append(error_msg)
|
||||||
|
|
||||||
# 第二步:关闭浏览器进程
|
# 第二步:关闭浏览器进程
|
||||||
if self.browser:
|
if self.browser:
|
||||||
try:
|
try:
|
||||||
self.browser.close()
|
self.browser.close()
|
||||||
# self.log("浏览器进程已关闭") # 精简日志
|
# self.log("浏览器进程已关闭") # 精简日志
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"关闭浏览器时出错: {str(e)}"
|
error_msg = f"关闭浏览器时出错: {str(e)}"
|
||||||
self.log(error_msg)
|
self.log(error_msg)
|
||||||
errors.append(error_msg)
|
errors.append(error_msg)
|
||||||
|
|
||||||
# 第三步:停止Playwright
|
# 第三步:停止Playwright
|
||||||
if self.playwright:
|
if self.playwright:
|
||||||
try:
|
try:
|
||||||
self.playwright.stop()
|
self.playwright.stop()
|
||||||
# self.log("Playwright已停止") # 精简日志
|
# self.log("Playwright已停止") # 精简日志
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"停止Playwright时出错: {str(e)}"
|
error_msg = f"停止Playwright时出错: {str(e)}"
|
||||||
self.log(error_msg)
|
self.log(error_msg)
|
||||||
errors.append(error_msg)
|
errors.append(error_msg)
|
||||||
|
|
||||||
# 第四步:清空引用,确保垃圾回收
|
# 第四步:清空引用,确保垃圾回收
|
||||||
self.context = None
|
self.context = None
|
||||||
self.page = None
|
self.page = None
|
||||||
self.main_page = None
|
self.main_page = None
|
||||||
self.browser = None
|
self.browser = None
|
||||||
self.playwright = None
|
self.playwright = None
|
||||||
|
|
||||||
# 第五步:强制等待,确保进程完全退出
|
# 第五步:强制等待,确保进程完全退出
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
@@ -1383,18 +1388,24 @@ class PlaywrightAutomation:
|
|||||||
|
|
||||||
def _cleanup_on_exit(self):
|
def _cleanup_on_exit(self):
|
||||||
"""进程退出时的清理函数(由atexit调用)"""
|
"""进程退出时的清理函数(由atexit调用)"""
|
||||||
if not self._closed:
|
# Bug #13 fix: 尝试获取锁,但不阻塞(避免退出时死锁)
|
||||||
try:
|
acquired = self._lock.acquire(blocking=False)
|
||||||
# 静默关闭,避免在退出时产生过多日志
|
try:
|
||||||
if self.context:
|
if not self._closed:
|
||||||
self.context.close()
|
try:
|
||||||
if self.browser:
|
# 静默关闭,避免在退出时产生过多日志
|
||||||
self.browser.close()
|
if self.context:
|
||||||
if self.playwright:
|
self.context.close()
|
||||||
self.playwright.stop()
|
if self.browser:
|
||||||
self._closed = True
|
self.browser.close()
|
||||||
except:
|
if self.playwright:
|
||||||
pass # 退出时忽略所有错误
|
self.playwright.stop()
|
||||||
|
self._closed = True
|
||||||
|
except:
|
||||||
|
pass # 退出时忽略所有错误
|
||||||
|
finally:
|
||||||
|
if acquired:
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
"""Context manager支持 - 进入"""
|
"""Context manager支持 - 进入"""
|
||||||
|
|||||||
Reference in New Issue
Block a user