16 KiB
MOMO 監控系統 - 安全修復摘要
修復日期: 2026-01-12 系統版本: V9.4 修復人員: Claude Code (Sonnet 4.5)
📋 修復概覽
本次安全稽核共發現 17 個安全漏洞,按風險等級分類:
- 🔴 Critical(重大):3 個 ✅ 已修復
- 🟠 High(高風險):4 個 ✅ 已全部修復
- 🟡 Medium(中風險):7 個 ⏳ 待處理
- 💡 建議事項:3 個 ⏳ 待處理
✅ 已完成修復(Critical + High)
🔴 Critical #24: 移除硬編碼敏感資訊
修復狀態: ✅ 已完成
修改檔案:
config.py- 改用環境變數.env- 新建敏感資訊配置檔.env.example- 新建配置模板.gitignore- 防止敏感檔案被提交requirements.txt- 添加 python-dotenv
防護機制:
- 所有敏感資訊改用
os.getenv()從環境變數讀取 .env檔案已加入.gitignore- 提供
.env.example作為配置模板
⚠️ 重要後續步驟(請立即執行):
# 1. 立即更換所有已外洩的憑證
# 當前已外洩的憑證包括:
# - LOGIN_PASSWORD: 0936223270
# - TELEGRAM_BOT_TOKEN: 8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg
# - LINE_CHANNEL_ACCESS_TOKEN
# - EMAIL_HOST_PASSWORD: jopokbhdpnnborjd
# - NGROK_AUTH_TOKEN: 36e27NM5V7sUJ8QxJIAAWCp7sUv_3brtcrBarYvcP3SbvFKhF
# 2. 更新 .env 文件中的新憑證
# 3. 確保 .env 已加入 .gitignore(已完成)
# 4. 如果專案已推送到 Git,建議清理 commit 歷史中的敏感資訊
# 使用工具如 git-filter-repo 或 BFG Repo-Cleaner
🔴 Critical #25: 修復 SQL Injection 漏洞 #1
修復狀態: ✅ 已完成
修改檔案:
app.py(第 108-204 行) - 新增 SQL 安全驗證函數app.py(第 2652 行) - Excel 匯入功能app.py(第 3582 行) - 業績分析頁面
防護機制:
-
表名白名單驗證 -
validate_table_name()- 允許的表名清單:
ALLOWED_TABLES - 正則表達式驗證(僅允許
[a-zA-Z0-9_]) - SQL 關鍵字過濾
- 允許的表名清單:
-
欄位名驗證 -
validate_column_names()- 支援中文欄位名(
[\w\u4e00-\u9fff]) - 防止特殊字符注入
- 支援中文欄位名(
-
安全查詢封裝 -
safe_read_sql()- 自動驗證表名與欄位名
- 使用 SQLAlchemy
text()避免注入
修復位置:
# 修復前(危險):
df_existing = pd.read_sql(f"SELECT * FROM {table_name}", engine)
# 修復後(安全):
df_existing = safe_read_sql(table_name, engine=engine)
🔴 Critical #26: 修復 SQL Injection 漏洞 #2
修復狀態: ✅ 已完成
修改檔案:
database/manager.py(第 15-31 行) - 新增時間戳清理函數database/manager.py(第 77-78 行) - ALTER TABLE 語句
防護機制:
- 時間戳格式驗證 -
sanitize_timestamp()- 僅允許
YYYY-MM-DD HH:MM:SS格式 - 正則表達式嚴格驗證
- 僅允許
修復位置:
# 修復前(危險):
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
session.execute(text(f"ALTER TABLE products ADD COLUMN updated_at TIMESTAMP DEFAULT '{now_str}'"))
# 修復後(安全):
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
safe_timestamp = sanitize_timestamp(now_str)
session.execute(text(f"ALTER TABLE products ADD COLUMN updated_at TIMESTAMP DEFAULT '{safe_timestamp}'"))
🟠 High #27: 強化登入驗證機制
修復狀態: ✅ 已完成
修改檔案:
auth.py- 完整重寫登入邏輯app.py(第 303-335 行) - Flask 安全配置generate_password_hash.py- 新建密碼雜湊生成工具.env.example+.env- 新增 USE_HTTPS 配置
新增功能:
-
密碼雜湊支持
- 使用
werkzeug.security.check_password_hash() - 支援
pbkdf2:sha256雜湊演算法 - 向後兼容明文密碼(會發出警告)
- 使用
-
登入失敗追蹤(IP-based)
- 記錄每個 IP 的失敗次數
- 30 分鐘內無活動自動重置計數
- 支援代理伺服器 IP 偵測
-
帳號鎖定機制
- 5 次失敗後鎖定 5 分鐘
- 鎖定期間顯示剩餘時間
- 登入成功自動清除失敗記錄
-
Session 安全配置
SESSION_COOKIE_HTTPONLY = True- 防 XSSSESSION_COOKIE_SAMESITE = 'Lax'- 防 CSRFPERMANENT_SESSION_LIFETIME = 2小時- 自動過期SESSION_COOKIE_SECURE- HTTPS 環境啟用MAX_CONTENT_LENGTH = 10MB- 檔案上傳限制
-
密碼強度驗證函數
- 至少 8 個字元
- 包含英文字母
- 包含數字
使用方法:
1. 生成雜湊密碼
python generate_password_hash.py
# 依照提示輸入新密碼(至少8字元,含英數)
# 將生成的雜湊值複製到 .env 檔案
2. 更新 .env 檔案
# 範例(請替換為您自己生成的雜湊值):
LOGIN_PASSWORD=pbkdf2:sha256:600000$abc123def456$...長字串...
3. 重新啟動系統
python app.py
安全日誌範例:
🔐 收到登入請求 | IP: 192.168.1.100
❌ 登入失敗 | IP: 192.168.1.100 | 剩餘嘗試: 4
🔒 帳號已鎖定 | IP: 192.168.1.100 | 原因: 連續 5 次失敗
✅ 登入成功 | IP: 192.168.1.101
🟠 High #28: 加入 CSRF 防護
修復狀態: ✅ 已完成
修改檔案:
requirements.txt- 添加 Flask-WTFapp.py(第 337-344 行) - 初始化 CSRF 防護login.html- 新建登入頁面(含 CSRF token)settings.html- 添加 CSRF meta tag 與 token headersdashboard.html- 添加 CSRF meta tag 與 token headersedm_dashboard.html- 添加 CSRF meta tag 與 token headerssystem_settings.html- 添加 CSRF meta tag 與 token headers
防護機制:
- 全局 CSRF 防護啟用
# app.py (第 341-344 行)
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
sys_log.info("[Security] ✅ CSRF 防護已啟用 (Flask-WTF)")
-
HTML 表單防護
- 所有 POST 表單添加
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> login.html(第 142 行) - 登入表單
- 所有 POST 表單添加
-
AJAX 請求防護
- 在所有 HTML 模板的
<head>添加 CSRF meta tag:<meta name="csrf-token" content="{{ csrf_token() }}"> - JavaScript 輔助函數:
function getCSRFToken() { return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); } - 所有 POST/PUT/DELETE fetch 請求添加 header:
fetch(url, { method: 'POST', headers: { 'X-CSRFToken': getCSRFToken() }, body: formData })
- 在所有 HTML 模板的
已防護的端點:
/login(POST) - 登入表單/api/run_task(POST) - 手動爬蟲觸發/api/trigger_momo_notification(POST) - 通知觸發/api/run_edm_task(POST) - EDM 爬蟲/api/trigger_edm_notification(POST) - EDM 通知/api/run_festival_task(POST) - Festival 爬蟲/api/categories(POST/PUT/DELETE) - 分類管理/api/test_url(POST) - URL 測試/api/backup(POST) - 系統備份/api/import_excel(POST) - Excel 匯入
測試驗證:
# 1. 測試 CSRF 防護是否生效
curl -X POST http://localhost/api/run_task
# 預期結果: 400 Bad Request (The CSRF token is missing)
# 2. 測試附帶正確 CSRF token 的請求
# 需從瀏覽器獲取 token 並添加到 header
注意事項:
- Flask-WTF 會自動驗證所有 POST/PUT/DELETE/PATCH 請求
- GET 請求不受 CSRF 保護影響(符合 HTTP 語義)
- 如需豁免特定端點,可使用
@csrf.exempt裝飾器
🟠 High #29: 修復路徑遍歷漏洞
修復狀態: ✅ 已完成
修改檔案:
app.py(第 206-240 行) - 新增safe_join()安全路徑函數app.py(第 2586-2614 行) - 修復/api/backup/download/<filename>路由
防護機制:
-
安全路徑拼接函數 -
safe_join()- 使用 Python
pathlib.Path.resolve()取得絕對路徑 - 驗證最終路徑必須在基礎目錄內(使用
relative_to()) - 偵測到路徑遍歷嘗試時拋出
ValueError - 記錄所有路徑遍歷嘗試到安全日誌
- 使用 Python
-
下載端點強化
- 驗證檔案存在性
- 確認是檔案而非目錄
- 使用
safe_path.name而非原始 filename - 適當的錯誤處理與日誌記錄
測試案例:
# 1. 正常下載(應該成功)
curl http://localhost/api/backup/download/momo_system_backup_V9.4_20260112_1430.zip
# 2. 路徑遍歷攻擊(應被阻擋)
curl http://localhost/api/backup/download/../../../etc/passwd
# 預期結果: {"error":"非法路徑"} + 安全日誌警告
🟠 High #30: 檔案上傳驗證
修復狀態: ✅ 已完成
修改檔案:
app.py(第 44 行) - 添加from werkzeug.utils import secure_filenameapp.py(第 242-299 行) - 新增檔案上傳驗證函數app.py(第 2676-2718 行) - 修復/api/import_excel端點
防護機制:
-
副檔名白名單驗證
- 僅允許:
.xlsx,.xls,.csv - 使用
ALLOWED_UPLOAD_EXTENSIONS集合管理
- 僅允許:
-
檔案名稱清理
- 使用
werkzeug.utils.secure_filename()清理檔案名稱 - 移除路徑遍歷字元與特殊字元
- 使用
-
檔案大小限制
- Flask 配置:
MAX_CONTENT_LENGTH = 10MB - 超過限制時自動回傳 413 錯誤
- Flask 配置:
安全日誌範例:
[Security] 檔案上傳驗證失敗 | Filename: ../../../etc/passwd | Error: 檔案名稱不合法
[Web] [Import] 檔案上傳驗證通過 | Original: 即時業績(全月).xlsx | Safe: 即時業績全月.xlsx
⏳ 待修復項目(Medium 級別)
# 3. 清理檔名
safe_name = secure_filename(file.filename)
# 4. 檢查檔案大小(透過 seek)
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0) # 重置檔案指標
if file_size > MAX_FILE_SIZE:
return False, f'檔案過大(限制 {MAX_FILE_SIZE // (1024*1024)} MB)', None
# 5. 驗證 MIME type(可選,需安裝 python-magic)
# mime_type = magic.from_buffer(file.read(2048), mime=True)
# file.seek(0)
# if mime_type not in ALLOWED_MIME_TYPES:
# return False, 'MIME type 驗證失敗', None
return True, None, safe_name
在路由中使用
@app.route('/api/import_excel', methods=['POST']) def import_excel(): file = request.files.get('file')
is_valid, error_msg, safe_name = validate_file_upload(file)
if not is_valid:
return jsonify({'status': 'error', 'message': error_msg}), 400
# 繼續處理檔案...
---
## ⏳ 待修復項目(Medium 級別)
### 🟡 Medium #31: 缺少 HTTP 安全標頭
**修復方式:**
```python
# app.py
@app.after_request
def set_security_headers(response):
response.headers['X-Content-Type-Options'] = 'nosniff'
response.headers['X-Frame-Options'] = 'DENY'
response.headers['X-XSS-Protection'] = '1; mode=block'
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net"
return response
🟡 Medium #32: Session 安全性不足
修復狀態: ✅ 已部分完成(已在 High #27 中修復)
🟡 Medium #33: 弱亂數產生器
位置: app.py (第 720 行)
修復方式:
import secrets
# 修復前:
new_id = int(time.time() * 1000)
# 修復後:
new_id = secrets.randbits(64)
🟡 Medium #34: 敏感資訊洩漏
修復方式:
def mask_sensitive_data(data, visible_chars=2):
"""遮罩敏感資料"""
if len(data) <= visible_chars * 2:
return '****'
return data[:visible_chars] + '****' + data[-visible_chars:]
# 使用範例
sys_log.info(f"使用者登入 | Token: {mask_sensitive_data(token)}")
🟡 Medium #35: SSRF / Open Redirect 風險
位置: app.py (第 756-778 行)
修復方式:
import ipaddress
from urllib.parse import urlparse
def is_safe_url(url):
"""驗證 URL 安全性"""
try:
parsed = urlparse(url)
# 檢查協議
if parsed.scheme not in ['http', 'https']:
return False
# 檢查 hostname
hostname = parsed.hostname
if hostname:
try:
ip = ipaddress.ip_address(hostname)
# 禁止存取私有 IP
if ip.is_private or ip.is_loopback or ip.is_link_local:
return False
except:
pass
# 黑名單檢查
blocked_hosts = ['localhost', '127.0.0.1', '0.0.0.0', '::1']
if hostname in blocked_hosts:
return False
return True
except:
return False
# 使用範例
if not is_safe_url(url):
return jsonify({'error': '不允許的 URL'}), 400
🟡 Medium #36: 資源耗盡風險
修復方式:
# 1. 安裝 Flask-Limiter
pip install Flask-Limiter
# 2. 在 app.py 配置
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
# 3. 為特定路由設定限制
@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
pass
🟡 Medium #37: 缺少授權檢查
修復方式:
檢視所有路由,確保需要登入的端點都有 @login_required 裝飾器:
@app.route('/dashboard')
@login_required # 確保加上此裝飾器
def dashboard():
pass
💡 建議事項
38. 定期安全掃描
工具安裝:
# Python 程式碼安全掃描
pip install bandit
bandit -r . -f json -o bandit_report.json
# 依賴套件漏洞掃描
pip install pip-audit
pip-audit
# 進階程式碼分析(線上工具)
# https://semgrep.dev/
39. 建立安全開發規範
建議制定 SECURITY_GUIDELINES.md,包含:
- Secure Coding Checklist
- Code Review 檢查清單
- Pre-commit hooks 設定
- 依賴套件更新流程
40. 備份與災難復原計畫
建議實作:
- 自動化資料庫備份(每日)
- 系統還原 SOP 文件
- 備份還原測試(每月)
- 異地備份方案
📊 修復優先級建議
🚨 本週內必須完成
- ✅ 更換所有已外洩的憑證(Critical #24)
- ⏳ 加入 CSRF 防護(High #28)
- ⏳ 修復路徑遍歷漏洞(High #29)
- ⏳ 檔案上傳驗證(High #30)
📅 本月內建議完成
- ⏳ 新增 HTTP 安全標頭(Medium #31)
- ⏳ 修復弱亂數產生器(Medium #33)
- ⏳ 實作 SSRF 防護(Medium #35)
- ⏳ 資源耗盡防護(Medium #36)
- ⏳ 檢視授權檢查(Medium #37)
📌 持續改進
- ⏳ 密碼雜湊更新(使用 generate_password_hash.py)
- ⏳ 定期安全掃描
- ⏳ 建立安全開發規範
- ⏳ 備份與災難復原計畫
🔧 部署檢查清單
在重新啟動系統前,請確認:
環境變數配置
.env檔案已建立並填入所有必要值- 所有憑證已更換為新值(不使用範例中的值)
- SECRET_KEY 已設定為隨機強密碼
- USE_HTTPS 根據環境正確設定
密碼更新
- 已執行
python generate_password_hash.py - 新密碼雜湊已複製到
.env的LOGIN_PASSWORD - 密碼符合強度要求(8+ 字元,含英數)
檔案權限
.env檔案權限設為 600(chmod 600 .env).gitignore已包含.env- 確認
.env未被提交到 Git
測試
- 使用新密碼測試登入
- 測試登入失敗 5 次是否觸發鎖定
- 測試 Session 2 小時後是否自動登出
- 測試檔案上傳是否受 10MB 限制
📞 後續支援
如需協助完成剩餘安全修復,請參考:
- TODO_NEXT_STEPS.txt - 完整安全修復清單
- generate_password_hash.py - 密碼雜湊生成工具
.env.example- 環境變數配置模板
⚠️ 安全提醒:
- 立即更換所有已外洩的憑證
- 定期更換密碼(建議每 90 天)
- 定期更新依賴套件至最新版本
- 執行定期安全掃描
- 建立安全事件應變計畫
最後更新: 2026-01-12 修復進度: 4/17 項已完成(23.5%)