Files
ewoooc/SECURITY_FIX_SUMMARY.md
OoO d88dcc8f75
All checks were successful
CD Pipeline / deploy (push) Successful in 1m45s
fix(devops): 清理舊端口與危險 compose 操作
2026-04-30 14:24:53 +08:00

16 KiB
Raw Blame History

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 行) - 業績分析頁面

防護機制:

  1. 表名白名單驗證 - validate_table_name()

    • 允許的表名清單:ALLOWED_TABLES
    • 正則表達式驗證(僅允許 [a-zA-Z0-9_]
    • SQL 關鍵字過濾
  2. 欄位名驗證 - validate_column_names()

    • 支援中文欄位名([\w\u4e00-\u9fff]
    • 防止特殊字符注入
  3. 安全查詢封裝 - 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 配置

新增功能:

  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. 生成雜湊密碼

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-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 防護啟用
# app.py (第 341-344 行)
from flask_wtf.csrf import CSRFProtect

csrf = CSRFProtect(app)
sys_log.info("[Security] ✅ CSRF 防護已啟用 (Flask-WTF)")
  1. HTML 表單防護

    • 所有 POST 表單添加 <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
    • login.html (第 142 行) - 登入表單
  2. 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
      })
      

已防護的端點:

  • /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> 路由

防護機制:

  1. 安全路徑拼接函數 - safe_join()

    • 使用 Python pathlib.Path.resolve() 取得絕對路徑
    • 驗證最終路徑必須在基礎目錄內(使用 relative_to()
    • 偵測到路徑遍歷嘗試時拋出 ValueError
    • 記錄所有路徑遍歷嘗試到安全日誌
  2. 下載端點強化

    • 驗證檔案存在性
    • 確認是檔案而非目錄
    • 使用 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_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 行)

修復方式:

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 文件
  • 備份還原測試(每月)
  • 異地備份方案

📊 修復優先級建議

🚨 本週內必須完成

  1. 更換所有已外洩的憑證Critical #24
  2. 加入 CSRF 防護High #28
  3. 修復路徑遍歷漏洞High #29
  4. 檔案上傳驗證High #30

📅 本月內建議完成

  1. 新增 HTTP 安全標頭Medium #31
  2. 修復弱亂數產生器Medium #33
  3. 實作 SSRF 防護Medium #35
  4. 資源耗盡防護Medium #36
  5. 檢視授權檢查Medium #37

📌 持續改進

  1. 密碼雜湊更新(使用 generate_password_hash.py
  2. 定期安全掃描
  3. 建立安全開發規範
  4. 備份與災難復原計畫

🔧 部署檢查清單

在重新啟動系統前,請確認:

環境變數配置

  • .env 檔案已建立並填入所有必要值
  • 所有憑證已更換為新值(不使用範例中的值)
  • SECRET_KEY 已設定為隨機強密碼
  • USE_HTTPS 根據環境正確設定

密碼更新

  • 已執行 python generate_password_hash.py
  • 新密碼雜湊已複製到 .envLOGIN_PASSWORD
  • 密碼符合強度要求8+ 字元,含英數)

檔案權限

  • .env 檔案權限設為 600chmod 600 .env
  • .gitignore 已包含 .env
  • 確認 .env 未被提交到 Git

測試

  • 使用新密碼測試登入
  • 測試登入失敗 5 次是否觸發鎖定
  • 測試 Session 2 小時後是否自動登出
  • 測試檔案上傳是否受 10MB 限制

📞 後續支援

如需協助完成剩餘安全修復,請參考:


⚠️ 安全提醒:

  1. 立即更換所有已外洩的憑證
  2. 定期更換密碼(建議每 90 天)
  3. 定期更新依賴套件至最新版本
  4. 執行定期安全掃描
  5. 建立安全事件應變計畫

最後更新: 2026-01-12 修復進度: 4/17 項已完成23.5%