# 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: <LOGIN_PASSWORD>
#   - TELEGRAM_BOT_TOKEN: <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 表單添加 `<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>`
   - `login.html` (第 142 行) - 登入表單

3. **AJAX 請求防護**
   - 在所有 HTML 模板的 `<head>` 添加 CSRF meta tag：
     ```html
     <meta name="csrf-token" content="{{ csrf_token() }}">
     ```
   - 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/<filename>` 路由

**防護機制：**

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%）
