# 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` 作為配置模板 **⚠️ 重要後續步驟(請立即執行):** ```bash # 1. 立即更換所有已外洩的憑證 # 當前已外洩的憑證包括: # - LOGIN_PASSWORD: # - TELEGRAM_BOT_TOKEN: # - 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 行) - 業績分析頁面 **防護機制:** 1. **表名白名單驗證** - `validate_table_name()` - 允許的表名清單:`ALLOWED_TABLES` - 正則表達式驗證(僅允許 `[a-zA-Z0-9_]`) - SQL 關鍵字過濾 2. **欄位名驗證** - `validate_column_names()` - 支援中文欄位名(`[\w\u4e00-\u9fff]`) - 防止特殊字符注入 3. **安全查詢封裝** - `safe_read_sql()` - 自動驗證表名與欄位名 - 使用 SQLAlchemy `text()` 避免注入 **修復位置:** ```python # 修復前(危險): 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` 格式 - 正則表達式嚴格驗證 **修復位置:** ```python # 修復前(危險): 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 配置 **新增功能:** 1. **密碼雜湊支持** - 使用 `werkzeug.security.check_password_hash()` - 支援 `pbkdf2:sha256` 雜湊演算法 - 向後兼容明文密碼(會發出警告) 2. **登入失敗追蹤(IP-based)** - 記錄每個 IP 的失敗次數 - 30 分鐘內無活動自動重置計數 - 支援代理伺服器 IP 偵測 3. **帳號鎖定機制** - 5 次失敗後鎖定 5 分鐘 - 鎖定期間顯示剩餘時間 - 登入成功自動清除失敗記錄 4. **Session 安全配置** - `SESSION_COOKIE_HTTPONLY = True` - 防 XSS - `SESSION_COOKIE_SAMESITE = 'Lax'` - 防 CSRF - `PERMANENT_SESSION_LIFETIME = 2小時` - 自動過期 - `SESSION_COOKIE_SECURE` - HTTPS 環境啟用 - `MAX_CONTENT_LENGTH = 10MB` - 檔案上傳限制 5. **密碼強度驗證函數** - 至少 8 個字元 - 包含英文字母 - 包含數字 **使用方法:** **1. 生成雜湊密碼** ```bash python generate_password_hash.py # 依照提示輸入新密碼(至少8字元,含英數) # 將生成的雜湊值複製到 .env 檔案 ``` **2. 更新 .env 檔案** ```bash # 範例(請替換為您自己生成的雜湊值): LOGIN_PASSWORD=pbkdf2:sha256:600000$abc123def456$...長字串... ``` **3. 重新啟動系統** ```bash 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-WTF - `app.py` (第 337-344 行) - 初始化 CSRF 防護 - `login.html` - 新建登入頁面(含 CSRF token) - `settings.html` - 添加 CSRF meta tag 與 token headers - `dashboard.html` - 添加 CSRF meta tag 與 token headers - `edm_dashboard.html` - 添加 CSRF meta tag 與 token headers - `system_settings.html` - 添加 CSRF meta tag 與 token headers **防護機制:** 1. **全局 CSRF 防護啟用** ```python # app.py (第 341-344 行) from flask_wtf.csrf import CSRFProtect csrf = CSRFProtect(app) sys_log.info("[Security] ✅ CSRF 防護已啟用 (Flask-WTF)") ``` 2. **HTML 表單防護** - 所有 POST 表單添加 `` - `login.html` (第 142 行) - 登入表單 3. **AJAX 請求防護** - 在所有 HTML 模板的 `` 添加 CSRF meta tag: ```html ``` - JavaScript 輔助函數: ```javascript function getCSRFToken() { return document.querySelector('meta[name="csrf-token"]').getAttribute('content'); } ``` - 所有 POST/PUT/DELETE fetch 請求添加 header: ```javascript fetch(url, { method: 'POST', headers: { 'X-CSRFToken': getCSRFToken() }, body: formData }) ``` **已防護的端點:** - `/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 匯入 **測試驗證:** ```bash # 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/` 路由 **防護機制:** 1. **安全路徑拼接函數** - `safe_join()` - 使用 Python `pathlib.Path.resolve()` 取得絕對路徑 - 驗證最終路徑必須在基礎目錄內(使用 `relative_to()`) - 偵測到路徑遍歷嘗試時拋出 `ValueError` - 記錄所有路徑遍歷嘗試到安全日誌 2. **下載端點強化** - 驗證檔案存在性 - 確認是檔案而非目錄 - 使用 `safe_path.name` 而非原始 filename - 適當的錯誤處理與日誌記錄 **測試案例:** ```bash # 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_filename` - `app.py` (第 242-299 行) - 新增檔案上傳驗證函數 - `app.py` (第 2676-2718 行) - 修復 `/api/import_excel` 端點 **防護機制:** 1. **副檔名白名單驗證** - 僅允許: `.xlsx`, `.xls`, `.csv` - 使用 `ALLOWED_UPLOAD_EXTENSIONS` 集合管理 2. **檔案名稱清理** - 使用 `werkzeug.utils.secure_filename()` 清理檔案名稱 - 移除路徑遍歷字元與特殊字元 3. **檔案大小限制** - Flask 配置: `MAX_CONTENT_LENGTH = 10MB` - 超過限制時自動回傳 413 錯誤 **安全日誌範例:** ``` [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 行) **修復方式:** ```python import secrets # 修復前: new_id = int(time.time() * 1000) # 修復後: new_id = secrets.randbits(64) ``` ### 🟡 Medium #34: 敏感資訊洩漏 **修復方式:** ```python 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 行) **修復方式:** ```python 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: 資源耗盡風險 **修復方式:** ```python # 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` 裝飾器: ```python @app.route('/dashboard') @login_required # 確保加上此裝飾器 def dashboard(): pass ``` --- ## 💡 建議事項 ### 38. 定期安全掃描 **工具安裝:** ```bash # 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 文件 - 備份還原測試(每月) - 異地備份方案 --- ## 📊 修復優先級建議 ### 🚨 本週內必須完成 1. ✅ 更換所有已外洩的憑證(Critical #24) 2. ⏳ 加入 CSRF 防護(High #28) 3. ⏳ 修復路徑遍歷漏洞(High #29) 4. ⏳ 檔案上傳驗證(High #30) ### 📅 本月內建議完成 5. ⏳ 新增 HTTP 安全標頭(Medium #31) 6. ⏳ 修復弱亂數產生器(Medium #33) 7. ⏳ 實作 SSRF 防護(Medium #35) 8. ⏳ 資源耗盡防護(Medium #36) 9. ⏳ 檢視授權檢查(Medium #37) ### 📌 持續改進 10. ⏳ 密碼雜湊更新(使用 generate_password_hash.py) 11. ⏳ 定期安全掃描 12. ⏳ 建立安全開發規範 13. ⏳ 備份與災難復原計畫 --- ## 🔧 部署檢查清單 在重新啟動系統前,請確認: ### 環境變數配置 - [ ] `.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](TODO_NEXT_STEPS.txt) - 完整安全修復清單 - [generate_password_hash.py](generate_password_hash.py) - 密碼雜湊生成工具 - `.env.example` - 環境變數配置模板 --- **⚠️ 安全提醒:** 1. 立即更換所有已外洩的憑證 2. 定期更換密碼(建議每 90 天) 3. 定期更新依賴套件至最新版本 4. 執行定期安全掃描 5. 建立安全事件應變計畫 **最後更新:** 2026-01-12 **修復進度:** 4/17 項已完成(23.5%)