修复关键安全漏洞(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 c1a65debbc
commit 716111e7e6
2 changed files with 149 additions and 57 deletions

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,14 +435,16 @@ class PlaywrightAutomation:
"""
for attempt in range(max_retries if retry else 1):
try:
# 先检查main_page是否有效
if not self.main_page:
self.log("⚠ main_page无效")
return None
# Bug #13 fix: 使用锁保护main_page访问
with self._lock:
# 先检查main_page是否有效
if not self.main_page:
self.log("⚠ main_page无效")
return None
iframe = self.main_page.frame('mainframe')
if iframe:
return iframe
iframe = self.main_page.frame('mainframe')
if iframe:
return iframe
except Exception as e:
error_msg = str(e)
if self.is_context_error(error_msg):
@@ -1327,49 +1330,51 @@ class PlaywrightAutomation:
def close(self):
"""完全关闭浏览器进程(每个账号独立)并确保资源释放"""
# 防止重复关闭
if self._closed:
return
self._closed = True
# Bug #13 fix: 使用锁保护close操作
with self._lock:
# 防止重复关闭
if self._closed:
return
self._closed = True
errors = []
errors = []
# 第一步:关闭上下文
if self.context:
try:
self.context.close()
# self.log("上下文已关闭") # 精简日志
except Exception as e:
error_msg = f"关闭上下文时出错: {str(e)}"
self.log(error_msg)
errors.append(error_msg)
# 第一步:关闭上下文
if self.context:
try:
self.context.close()
# self.log("上下文已关闭") # 精简日志
except Exception as e:
error_msg = f"关闭上下文时出错: {str(e)}"
self.log(error_msg)
errors.append(error_msg)
# 第二步:关闭浏览器进程
if self.browser:
try:
self.browser.close()
# self.log("浏览器进程已关闭") # 精简日志
except Exception as e:
error_msg = f"关闭浏览器时出错: {str(e)}"
self.log(error_msg)
errors.append(error_msg)
# 第二步:关闭浏览器进程
if self.browser:
try:
self.browser.close()
# self.log("浏览器进程已关闭") # 精简日志
except Exception as e:
error_msg = f"关闭浏览器时出错: {str(e)}"
self.log(error_msg)
errors.append(error_msg)
# 第三步:停止Playwright
if self.playwright:
try:
self.playwright.stop()
# self.log("Playwright已停止") # 精简日志
except Exception as e:
error_msg = f"停止Playwright时出错: {str(e)}"
self.log(error_msg)
errors.append(error_msg)
# 第三步:停止Playwright
if self.playwright:
try:
self.playwright.stop()
# self.log("Playwright已停止") # 精简日志
except Exception as e:
error_msg = f"停止Playwright时出错: {str(e)}"
self.log(error_msg)
errors.append(error_msg)
# 第四步:清空引用,确保垃圾回收
self.context = None
self.page = None
self.main_page = None
self.browser = None
self.playwright = None
# 第四步:清空引用,确保垃圾回收
self.context = None
self.page = None
self.main_page = None
self.browser = None
self.playwright = None
# 第五步:强制等待,确保进程完全退出
time.sleep(0.5)
@@ -1383,18 +1388,24 @@ class PlaywrightAutomation:
def _cleanup_on_exit(self):
"""进程退出时的清理函数由atexit调用"""
if not self._closed:
try:
# 静默关闭,避免在退出时产生过多日志
if self.context:
self.context.close()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
self._closed = True
except:
pass # 退出时忽略所有错误
# Bug #13 fix: 尝试获取锁,但不阻塞(避免退出时死锁)
acquired = self._lock.acquire(blocking=False)
try:
if not self._closed:
try:
# 静默关闭,避免在退出时产生过多日志
if self.context:
self.context.close()
if self.browser:
self.browser.close()
if self.playwright:
self.playwright.stop()
self._closed = True
except:
pass # 退出时忽略所有错误
finally:
if acquired:
self._lock.release()
def __enter__(self):
"""Context manager支持 - 进入"""