Files
ewoooc/SECURITY_FIX_SUMMARY.md
OoO d6d8777e41
All checks were successful
CD Pipeline / deploy (push) Successful in 1m12s
V10.601 收斂 Gemini 與密鑰治理
2026-06-06 14:52:46 +08:00

602 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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%