Files
daily-news-app/security-fixes.md
donald db0f0bbfe7 Initial commit: Daily News App
企業內部新聞彙整與分析系統
- 自動新聞抓取 (Digitimes, 經濟日報, 工商時報)
- AI 智慧摘要 (OpenAI/Claude/Ollama)
- 群組管理與訂閱通知
- 已清理 Python 快取檔案

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 23:53:24 +08:00

1177 lines
35 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.

# 每日報導 APP - 安全審計報告
> **審計日期:** 2025-01-27
> **審計人員:** 資深安全架構師
> **專案狀態:** 上線前安全審計
---
## 📋 基本專案資訊
### 專案概述
- **專案名稱:** 每日報導 APP
- **專案類型:** 企業內部新聞彙整與分析系統
- **目標用戶:** 系統管理員1-2位、市場分析專員1位、讀者40位
- **資料類型:**
- ✅ 處理個人識別資訊PII用戶帳號、Email、顯示名稱
- ❌ 不處理支付或金融資訊
- ✅ 有使用者生成內容UGC留言、標註
### 技術架構
- **前端:** React混合模式渲染、RESTful API
- **後端:** FastAPI (Python 3.11+)
- **資料庫:** MySQL 8.0
- **部署環境:** 地端部署1Panel 管理介面)
- **外部服務:** Google Gemini API / OpenAI API / Ollama、SMTP、LDAP/AD
### 依賴套件
- **主要依賴:** FastAPI, SQLAlchemy, python-jose, passlib, ldap3, httpx, beautifulsoup4
- **依賴檔案:** `requirements.txt`
---
## 🔴 第一部分:災難級新手錯誤檢查
### 威脅 1缺少 .gitignore 檔案
**風險等級:** `High`
**威脅描述:**
專案根目錄缺少 `.gitignore` 檔案,這意味著敏感檔案(如 `.env``__pycache__`、資料庫檔案)可能被意外提交到版本控制系統,導致敏感資訊洩露。
**受影響組件:**
- 專案根目錄
- 所有敏感檔案(`.env``*.db``__pycache__/``*.pyc`
**駭客劇本:**
> 我是一個普通的開發者,或者更糟,我是一個惡意攻擊者。我發現這個專案在 GitHub 或其他公開的 Git 倉庫中。我只需要執行 `git clone`,然後檢查歷史記錄。如果開發者曾經提交過 `.env` 檔案即使後來刪除了Git 歷史中仍然保留著。我可以用 `git log --all --full-history -- .env` 找到所有歷史版本,然後用 `git show <commit>:.env` 查看內容。瞬間我獲得了資料庫密碼、API Keys、JWT 密鑰等所有敏感資訊。我可以用這些資訊直接連接到生產資料庫,或者使用 API Keys 進行未授權的服務呼叫,所有費用都會記在您的帳上。
**修復原理:**
> `.gitignore` 就像您家門口的「禁止入內」標誌。它告訴 Git「這些檔案和目錄不要追蹤不要提交」。即使開發者不小心執行 `git add .`,被 `.gitignore` 列出的檔案也不會被加入。正確的做法是在專案一開始就建立 `.gitignore`,並且定期檢查是否有敏感檔案被意外提交。如果已經提交了,需要從 Git 歷史中完全移除(使用 `git filter-branch` 或 `git filter-repo`)。
**修復建議與程式碼範例:**
建立 `.gitignore` 檔案:
```gitignore
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# 環境變數與敏感資訊
.env
.env.local
.env.*.local
*.env
.envrc
# 資料庫
*.db
*.sqlite
*.sqlite3
*.db-journal
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# 日誌
*.log
logs/
*.log.*
# 上傳檔案
uploads/
!uploads/.gitkeep
# 測試
.pytest_cache/
.coverage
htmlcov/
.tox/
# 系統檔案
.DS_Store
Thumbs.db
# Docker
docker-compose.override.yml
# 備份檔案
*.bak
*.backup
*.old
```
**檢查已提交的敏感檔案:**
```bash
# 檢查是否有敏感檔案被提交
git log --all --full-history -- .env
git log --all --full-history -- "*.db"
git log --all --full-history -- "*.log"
# 如果發現,需要從歷史中移除(謹慎操作)
# 建議使用 git filter-repo 工具
```
---
### 威脅 2缺少 .env.example 檔案
**風險等級:** `Medium`
**威脅描述:**
專案缺少 `.env.example` 檔案,新開發者或部署人員無法知道需要設定哪些環境變數,可能導致:
1. 遺漏重要的環境變數設定
2. 使用預設值(如 `secret_key: "change-me-in-production"`)直接上線
3. 配置錯誤導致系統無法正常運作
**受影響組件:**
- 部署流程
- 新開發者入職
**修復建議:**
建立 `.env.example` 檔案:
```env
# 應用程式設定
APP_ENV=production
DEBUG=false
SECRET_KEY=your-secret-key-here-min-32-chars
JWT_SECRET_KEY=your-jwt-secret-key-here-min-32-chars
# 資料庫連線
DB_HOST=mysql.theaken.com
DB_PORT=33306
DB_NAME=db_A101
DB_USER=A101
DB_PASSWORD=your-database-password
# Redis 連線
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Celery
CELERY_BROKER_URL=redis://localhost:6379/0
CELERY_RESULT_BACKEND=redis://localhost:6379/0
# SMTP 設定
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=your-smtp-username
SMTP_PASSWORD=your-smtp-password
SMTP_FROM_EMAIL=noreply@example.com
SMTP_FROM_NAME=每日報導系統
SMTP_USE_TLS=true
# LDAP/AD 設定
LDAP_SERVER=ldap.example.com
LDAP_PORT=389
LDAP_BASE_DN=DC=example,DC=com
LDAP_BIND_DN=
LDAP_BIND_PASSWORD=
# LLM 設定(選擇一個)
LLM_PROVIDER=gemini
GEMINI_API_KEY=your-gemini-api-key
GEMINI_MODEL=gemini-1.5-pro
# 或使用 OpenAI
# LLM_PROVIDER=openai
# OPENAI_API_KEY=your-openai-api-key
# OPENAI_MODEL=gpt-4o
# 或使用 Ollama地端
# LLM_PROVIDER=ollama
# OLLAMA_ENDPOINT=http://localhost:11434
# OLLAMA_MODEL=llama3
# Digitimes 帳號
DIGITIMES_USERNAME=your-digitimes-username
DIGITIMES_PASSWORD=your-digitimes-password
```
---
### 威脅 3硬編碼的預設密鑰
**風險等級:** `High`
**威脅描述:**
`app/core/config.py` 中發現硬編碼的預設密鑰:
- `secret_key: str = "change-me-in-production"`
- `jwt_secret_key: str = "change-me"`
如果生產環境使用這些預設值,攻擊者可以:
1. 偽造 JWT Token
2. 解密儲存的敏感資料
3. 完全控制系統
**受影響組件:**
- `app/core/config.py` 第 22、44 行
**駭客劇本:**
> 我是一個攻擊者,我發現了這個應用程式。我嘗試登入,但沒有成功。沒關係,我檢查了應用程式的錯誤訊息或日誌,發現它使用 JWT 認證。我知道很多開發者會忘記更改預設的 JWT 密鑰。我下載了這個專案的原始碼(或者從公開的 Git 倉庫),看到 `jwt_secret_key = "change-me"`。太好了!現在我可以:
> 1. 使用這個密鑰偽造任何用戶的 JWT Token
> 2. 將 `user_id` 設為管理員的 ID`role` 設為 "admin"
> 3. 用這個偽造的 Token 訪問所有管理功能
> 4. 刪除所有用戶、修改所有報告、取得所有敏感資料
>
> 整個過程只需要幾分鐘,而且完全不需要破解密碼。
**修復原理:**
> 預設密鑰就像您家門的萬能鑰匙,每個人都知道。如果生產環境使用預設值,任何人都可以用這個「萬能鑰匙」進入您的系統。正確的做法是:
> 1. 預設值應該在開發環境才使用,並且明確標註
> 2. 生產環境必須透過環境變數提供強隨機密鑰
> 3. 密鑰應該至少 32 字元,使用加密安全的隨機數生成器產生
> 4. 不同環境(開發、測試、生產)應該使用不同的密鑰
**修復建議與程式碼範例:**
修改 `app/core/config.py`
```python
# 修改前
secret_key: str = "change-me-in-production"
jwt_secret_key: str = "change-me"
# 修改後
secret_key: str = Field(
default="change-me-in-production",
description="應用程式密鑰,生產環境必須透過環境變數設定"
)
jwt_secret_key: str = Field(
default="change-me",
description="JWT 簽章密鑰,生產環境必須透過環境變數設定"
)
# 在應用程式啟動時檢查
def validate_secrets():
"""驗證生產環境的密鑰設定"""
if settings.app_env == "production":
if settings.secret_key == "change-me-in-production":
raise ValueError("生產環境必須設定 SECRET_KEY 環境變數")
if settings.jwt_secret_key == "change-me":
raise ValueError("生產環境必須設定 JWT_SECRET_KEY 環境變數")
if len(settings.secret_key) < 32:
raise ValueError("SECRET_KEY 長度必須至少 32 字元")
if len(settings.jwt_secret_key) < 32:
raise ValueError("JWT_SECRET_KEY 長度必須至少 32 字元")
```
`app/main.py` 中呼叫驗證:
```python
@asynccontextmanager
async def lifespan(app: FastAPI):
"""應用程式生命週期管理"""
# 啟動時執行
from app.core.config import validate_secrets
validate_secrets() # 新增這行
print(f"🚀 {settings.app_name} 啟動中...")
# ... 其他程式碼
```
**產生強隨機密鑰的方法:**
```python
# 使用 Python 產生強隨機密鑰
import secrets
secret_key = secrets.token_urlsafe(32)
print(f"SECRET_KEY={secret_key}")
print(f"JWT_SECRET_KEY={secrets.token_urlsafe(32)}")
```
---
### 威脅 4Debug 模式在生產環境可能啟用
**風險等級:** `High`
**威脅描述:**
`app/core/config.py` 中,`debug: bool = True` 預設為 `True`。如果生產環境忘記設定 `DEBUG=false`,會導致:
1. 詳細的錯誤堆疊追蹤洩露給用戶
2. API 文件(`/docs`)公開暴露
3. 效能問題
4. 敏感資訊洩露
**受影響組件:**
- `app/core/config.py` 第 21 行
- `app/main.py` 第 32-33 行docs_url 和 redoc_url 的條件)
**駭客劇本:**
> 我訪問了這個應用程式的 API 文件頁面(`/docs`),發現它完全公開,不需要認證。太好了!我可以看到所有的 API 端點、參數格式、甚至可能的錯誤回應。更糟的是,如果 Debug 模式開啟,當我故意發送錯誤的請求時,系統會返回完整的堆疊追蹤,包括:
> - 檔案路徑(可能洩露伺服器結構)
> - 資料庫查詢(可能洩露資料庫結構)
> - 環境變數名稱(雖然不是值,但我知道系統使用什麼)
> - 內部函數名稱和邏輯流程
>
> 這些資訊讓我更容易找到漏洞和攻擊點。
**修復原理:**
> Debug 模式就像在公共場合大聲說出您的銀行帳號和密碼。它會暴露系統的內部運作細節,讓攻擊者更容易找到弱點。生產環境必須關閉 Debug 模式,並且:
> 1. 錯誤訊息應該對用戶友好但不洩露細節
> 2. 詳細錯誤應該記錄在伺服器日誌中,只有管理員可以查看
> 3. API 文件應該需要認證才能訪問,或者完全關閉
**修復建議與程式碼範例:**
修改 `app/core/config.py`
```python
# 修改前
debug: bool = True
# 修改後
debug: bool = Field(
default=False, # 預設為 False更安全
description="除錯模式,僅開發環境使用"
)
```
修改 `app/main.py`,加強安全檢查:
```python
def create_app() -> FastAPI:
"""建立 FastAPI 應用程式"""
# 生產環境強制關閉 Debug
if settings.app_env == "production" and settings.debug:
import warnings
warnings.warn("生產環境不應啟用 Debug 模式,已自動關閉")
settings.debug = False
app = FastAPI(
title=settings.app_name,
description="企業內部新聞彙整與分析系統 API",
version="1.0.0",
docs_url="/docs" if settings.debug else None, # 生產環境關閉
redoc_url="/redoc" if settings.debug else None, # 生產環境關閉
lifespan=lifespan
)
# 如果需要在生產環境保留 API 文件,應該加上認證
# 或者使用不同的路徑和認證中間件
```
---
### 威脅 5CORS 設定過於寬鬆(開發模式)
**風險等級:** `Medium`(開發環境) / `High`(生產環境)
**威脅描述:**
`app/main.py` 第 40 行,當 `debug=True`CORS 設定為 `allow_origins=["*"]`,允許所有來源的請求。如果生產環境意外啟用 Debug 模式,這會導致嚴重的安全問題。
**受影響組件:**
- `app/main.py` 第 38-44 行
**駭客劇本:**
> 我建立了一個惡意網站,當用戶訪問時,我的 JavaScript 會自動發送請求到您的 API。由於 CORS 設定為 `*`,瀏覽器不會阻擋這些請求。我可以:
> 1. 如果用戶已經登入您的系統,我可以使用他們的 Cookie 或 Token 發送請求
> 2. 執行跨站請求偽造CSRF攻擊
> 3. 竊取用戶的資料
> 4. 在用戶不知情的情況下執行操作(如刪除報告、修改設定)
**修復原理:**
> CORS跨來源資源共享就像您家門口的訪客名單。`allow_origins=["*"]` 意味著「任何人都可以進入」,這在開發環境可能方便,但在生產環境是災難性的。正確的做法是:
> 1. 生產環境必須明確列出允許的來源
> 2. 使用環境變數管理允許的來源列表
> 3. 開發環境可以使用 `*`,但必須確保生產環境不會使用這個設定
**修復建議與程式碼範例:**
修改 `app/core/config.py`,新增 CORS 設定:
```python
# 新增到 Settings 類別
cors_origins: list[str] = Field(
default=["http://localhost:3000", "http://localhost:8000"],
description="允許的 CORS 來源列表"
)
```
修改 `app/main.py`
```python
# 修改前
app.add_middleware(
CORSMiddleware,
allow_origins=["*"] if settings.debug else ["https://your-domain.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 修改後
# 生產環境必須明確指定來源
if settings.app_env == "production":
if "*" in settings.cors_origins:
raise ValueError("生產環境不允許使用 CORS origins = ['*']")
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins if not settings.debug else ["*"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
max_age=3600,
)
```
---
### 威脅 6敏感資訊在日誌中洩露
**風險等級:** `Medium`
**威脅描述:**
在多個檔案中使用 `print()` 輸出敏感資訊或詳細錯誤訊息,這些資訊可能被記錄到日誌檔案中,如果日誌檔案可公開訪問,會洩露敏感資訊。
**受影響組件:**
- `app/core/security.py` 第 97、100 行LDAP 錯誤)
- `app/services/notification_service.py` 第 23、44 行Email 錯誤)
- `app/services/crawler_service.py` 多處(抓取錯誤)
- `app/main.py` 第 18 行(資料庫連線資訊)
**修復建議:**
1. **使用標準 logging 模組替代 print()**
建立 `app/core/logging_config.py`
```python
import logging
import sys
from app.core.config import settings
def setup_logging():
"""設定日誌系統"""
log_level = logging.DEBUG if settings.debug else logging.INFO
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('logs/app.log', encoding='utf-8')
]
)
# 過濾敏感資訊
class SensitiveFilter(logging.Filter):
def filter(self, record):
# 移除可能包含敏感資訊的訊息
sensitive_keywords = ['password', 'secret', 'key', 'token', 'api_key']
msg = str(record.getMessage()).lower()
for keyword in sensitive_keywords:
if keyword in msg:
# 只記錄錯誤類型,不記錄詳細內容
record.msg = f"[敏感資訊已過濾] {record.name}"
record.args = ()
return True
# 應用過濾器到所有 logger
for handler in logging.root.handlers:
handler.addFilter(SensitiveFilter())
return logging.getLogger(__name__)
logger = setup_logging()
```
2. **修改所有使用 print() 的地方**
例如,修改 `app/core/security.py`
```python
# 修改前
except LDAPException as e:
print(f"LDAP Error: {e}")
return None
# 修改後
import logging
logger = logging.getLogger(__name__)
except LDAPException as e:
logger.error("LDAP 認證失敗", exc_info=True) # 不記錄詳細錯誤
return None
```
3. **修改 main.py不輸出資料庫連線資訊**
```python
# 修改前
print(f"🔗 資料庫: {settings.db_host}:{settings.db_port}/{settings.db_name}")
# 修改後
logger.info(f"🔗 資料庫連線: {settings.db_host}:{settings.db_port}/{settings.db_name[:3]}***") # 隱藏資料庫名稱
```
---
## 🟡 第二部分標準應用安全審計OWASP Top 10
### 威脅 7SQL 注入風險(低風險,但需注意)
**風險等級:** `Low`(已使用 ORM風險較低
**威脅描述:**
專案使用 SQLAlchemy ORM大部分查詢都是安全的。但需要檢查是否有使用原始 SQL 查詢的地方。
**檢查結果:**
✅ 未發現直接使用原始 SQL 查詢的程式碼
✅ 所有資料庫操作都透過 SQLAlchemy ORM
⚠️ 在 `scripts/init.sql` 中有原始 SQL但這是初始化腳本不影響運行時安全
**建議:**
1. 繼續使用 ORM避免使用原始 SQL
2. 如果必須使用原始 SQL必須使用參數化查詢
3. 定期進行程式碼審查,確保沒有引入原始 SQL
---
### 威脅 8LDAP 注入風險
**風險等級:** `Medium`
**威脅描述:**
`app/core/security.py` 第 75 行LDAP 搜尋過濾器直接使用用戶輸入,可能存在 LDAP 注入風險。
**受影響組件:**
- `app/core/security.py` 第 75 行
**修復建議:**
```python
# 修改前
search_filter = f"(sAMAccountName={username})"
# 修改後
from ldap3.utils.conv import escape_filter_chars
# 轉義特殊字元,防止 LDAP 注入
safe_username = escape_filter_chars(username)
search_filter = f"(sAMAccountName={safe_username})"
```
---
### 威脅 9檔案上傳安全風險
**風險等級:** `Medium`
**威脅描述:**
`app/api/v1/endpoints/settings.py` 第 145-171 行PDF Logo 上傳功能存在以下問題:
1. 檔案類型檢查僅依賴 `content_type`,可能被偽造
2. 檔案名稱直接使用,可能存在路徑遍歷風險
3. 沒有檔案大小限制
4. 沒有病毒掃描
**受影響組件:**
- `app/api/v1/endpoints/settings.py` 第 145-171 行
**修復建議:**
```python
import os
import hashlib
from pathlib import Path
from fastapi import UploadFile, File, HTTPException
import magic # 需要安裝 python-magic
@router.post("/pdf/logo")
async def upload_pdf_logo(
logo: UploadFile = File(...),
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin"))
):
"""上傳 PDF Logo"""
# 1. 檢查檔案大小(限制 5MB
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5MB
content = await logo.read()
if len(content) > MAX_FILE_SIZE:
raise HTTPException(status_code=400, detail="檔案大小超過 5MB 限制")
# 2. 使用 magic 檢查真實檔案類型(不依賴 content_type
file_type = magic.from_buffer(content, mime=True)
allowed_types = ["image/png", "image/jpeg", "image/svg+xml"]
if file_type not in allowed_types:
raise HTTPException(status_code=400, detail=f"不支援的檔案類型: {file_type}")
# 3. 使用安全的檔案名稱(使用 hash避免路徑遍歷
file_hash = hashlib.sha256(content).hexdigest()[:16]
file_ext = "png" if file_type == "image/png" else ("jpg" if file_type == "image/jpeg" else "svg")
safe_filename = f"company_logo_{file_hash}.{file_ext}"
# 4. 使用絕對路徑,避免路徑遍歷
upload_dir = Path("/app/uploads/logos").resolve() # 使用絕對路徑
upload_dir.mkdir(parents=True, exist_ok=True)
file_path = upload_dir / safe_filename
# 5. 確保檔案路徑在允許的目錄內(防止路徑遍歷)
if not str(file_path).startswith(str(upload_dir.resolve())):
raise HTTPException(status_code=400, detail="無效的檔案路徑")
# 6. 儲存檔案
with open(file_path, "wb") as f:
f.write(content)
# 7. 更新設定(使用相對路徑)
relative_path = f"uploads/logos/{safe_filename}"
set_setting_value(db, "pdf_logo_path", relative_path, current_user.id)
db.commit()
return {"logo_path": relative_path}
```
**需要安裝的套件:**
```bash
pip install python-magic-bin # Windows
# 或
pip install python-magic # Linux/Mac
```
---
### 威脅 10JWT Token 過期時間過長
**風險等級:** `Medium`
**威脅描述:**
`app/core/config.py` 第 46 行JWT Token 過期時間設定為 480 分鐘8 小時),這對於企業內部系統來說可能過長。如果 Token 被竊取,攻擊者有 8 小時的時間進行攻擊。
**受影響組件:**
- `app/core/config.py` 第 46 行
**修復建議:**
```python
# 修改前
jwt_access_token_expire_minutes: int = 480
# 修改後
jwt_access_token_expire_minutes: int = Field(
default=480, # 開發環境預設值
description="JWT Token 過期時間(分鐘),建議生產環境設為 60-120 分鐘"
)
# 在生產環境驗證
if settings.app_env == "production":
if settings.jwt_access_token_expire_minutes > 120:
import warnings
warnings.warn("生產環境 JWT Token 過期時間建議不超過 120 分鐘")
```
**進階建議:**
實作 Refresh Token 機制Access Token 設為較短時間15-30 分鐘Refresh Token 設為較長時間7 天),但需要額外的儲存和驗證機制。
---
### 威脅 11密碼強度檢查不足
**風險等級:** `Low`(內部系統,但仍需注意)
**威脅描述:**
`app/schemas/user.py` 中,密碼驗證僅檢查最小長度 6 字元,沒有檢查複雜度(大小寫、數字、特殊字元)。
**受影響組件:**
- `app/schemas/user.py` 第 39、49 行
**修復建議:**
建立 `app/utils/password_validator.py`
```python
import re
from typing import Tuple, bool
def validate_password_strength(password: str) -> Tuple[bool, str]:
"""
驗證密碼強度
Returns:
(is_valid, error_message)
"""
if len(password) < 8:
return False, "密碼長度必須至少 8 字元"
if len(password) > 128:
return False, "密碼長度不能超過 128 字元"
if not re.search(r'[a-z]', password):
return False, "密碼必須包含至少一個小寫字母"
if not re.search(r'[A-Z]', password):
return False, "密碼必須包含至少一個大寫字母"
if not re.search(r'\d', password):
return False, "密碼必須包含至少一個數字"
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
return False, "密碼必須包含至少一個特殊字元"
# 檢查常見弱密碼
common_passwords = ['password', '12345678', 'qwerty', 'admin']
if password.lower() in common_passwords:
return False, "不能使用常見的弱密碼"
return True, ""
```
`app/schemas/user.py` 中使用:
```python
from app.utils.password_validator import validate_password_strength
class UserCreate(BaseModel):
password: Optional[str] = Field(None, min_length=6)
@field_validator('password')
@classmethod
def validate_password(cls, v, info):
if info.data.get('auth_type') == 'local' and v:
is_valid, error_msg = validate_password_strength(v)
if not is_valid:
raise ValueError(error_msg)
return v
```
---
### 威脅 12缺少速率限制Rate Limiting
**風險等級:** `Medium`
**威脅描述:**
API 端點沒有速率限制,攻擊者可以:
1. 對登入端點進行暴力破解攻擊
2. 對 API 進行 DoS 攻擊
3. 大量請求導致系統資源耗盡
**修復建議:**
安裝 `slowapi`
```bash
pip install slowapi
```
`app/main.py` 中實作:
```python
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
# 在登入端點加上限制
@router.post("/login", response_model=LoginResponse)
@limiter.limit("5/minute") # 每分鐘最多 5 次
def login(request: Request, login_data: LoginRequest, db: Session = Depends(get_db)):
# ... 原有程式碼
```
---
### 威脅 13Email 內容未進行 XSS 防護
**風險等級:** `Low`(內部系統,但仍需注意)
**威脅描述:**
`app/services/notification_service.py`Email 內容直接使用報告標題和摘要,如果這些內容包含惡意腳本,可能在某些 Email 客戶端執行。
**受影響組件:**
- `app/services/notification_service.py` 第 48-99 行
**修復建議:**
```python
from html import escape
def create_report_email_content(report: Report, base_url: str = "") -> str:
"""建立報告通知 Email 內容"""
summary = report.edited_summary or report.ai_summary or "無摘要內容"
# 截取摘要前 500 字
if len(summary) > 500:
summary = summary[:500] + "..."
# 轉義 HTML 特殊字元,防止 XSS
safe_title = escape(report.title)
safe_group_name = escape(report.group.name)
safe_summary = escape(summary)
safe_base_url = escape(base_url)
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- ... 樣式 ... -->
</head>
<body>
<div class="container">
<div class="header">
<h1 style="margin:0;">每日報導</h1>
</div>
<div class="content">
<h2>{safe_title}</h2> <!-- 使用轉義後的值 -->
<p>
<strong>群組:</strong>{safe_group_name}<br>
<strong>日期:</strong>{report.report_date}
</p>
<div class="summary">
<h3>摘要</h3>
<p>{safe_summary}</p>
</div>
<p style="text-align: center; margin-top: 30px;">
<a href="{safe_base_url}/reports/{report.id}" class="button">閱讀完整報告</a>
</p>
</div>
<!-- ... -->
</div>
</body>
</html>
"""
return html
```
---
### 威脅 14缺少輸入驗證和清理
**風險等級:** `Low`(大部分使用 Pydantic但需檢查
**檢查結果:**
✅ 大部分 API 端點使用 Pydantic Schema 進行驗證
⚠️ 某些查詢參數(如 `search`)直接使用,沒有額外驗證
**修復建議:**
`app/api/v1/endpoints/users.py` 中:
```python
# 修改前
if search:
query = query.filter(
(User.username.ilike(f"%{search}%")) |
(User.display_name.ilike(f"%{search}%"))
)
# 修改後
if search:
# 清理輸入,移除特殊字元,防止注入
safe_search = search.strip()[:100] # 限制長度
# SQLAlchemy 的 ilike 已經使用參數化查詢,相對安全
# 但為了額外安全,可以進一步清理
safe_search = safe_search.replace('%', '\\%').replace('_', '\\_') # 轉義 SQL 萬用字元
query = query.filter(
(User.username.ilike(f"%{safe_search}%")) |
(User.display_name.ilike(f"%{safe_search}%"))
)
```
---
### 威脅 15依賴項安全性檢查
**風險等級:** `Medium`
**威脅描述:**
需要檢查 `requirements.txt` 中的依賴項是否有已知的安全漏洞。
**修復建議:**
1. **使用安全掃描工具:**
```bash
# 安裝 safety
pip install safety
# 掃描依賴項
safety check -r requirements.txt
```
2. **定期更新依賴項:**
```bash
# 使用 pip-auditPython 官方推薦)
pip install pip-audit
pip-audit -r requirements.txt
```
3. **使用 Dependabot 或類似工具自動檢查**
---
## 🟢 第三部分:業務邏輯漏洞
### 威脅 16AD 用戶自動建立可能導致權限提升
**風險等級:** `Medium`
**威脅描述:**
`app/api/v1/endpoints/auth.py` 第 84-103 行,首次 AD 登入時自動建立用戶,並預設分配「讀者」角色。如果攻擊者能夠偽造 AD 認證回應,可能自動建立帳號。
**受影響組件:**
- `app/api/v1/endpoints/auth.py` 第 84-103 行
**修復建議:**
1. **新增管理員審核機制:**
```python
# 修改後:首次 AD 登入需要管理員審核
if not user:
# 建立待審核用戶
user = User(
username=request.username,
display_name=ldap_result.get("display_name", request.username),
email=ldap_result.get("email"),
auth_type="ad",
role_id=reader_role.id,
is_active=False # 預設停用,需要管理員啟用
)
db.add(user)
db.commit()
# 通知管理員有新用戶待審核
# ... 發送通知 ...
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="帳號已建立,等待管理員審核啟用"
)
```
2. **或使用白名單機制:**
```python
# 檢查用戶是否在白名單中
allowed_ad_users = settings.allowed_ad_users.split(',') # 從環境變數讀取
if request.username not in allowed_ad_users:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="您的帳號尚未被授權使用此系統,請聯繫管理員"
)
```
---
### 威脅 17報告發布缺少時間驗證
**風險等級:** `Low`
**威脅描述:**
報告發布功能沒有檢查是否在規定的時間內(工作日 09:00 前),專員可能在任何時間發布報告。
**修復建議:**
`app/api/v1/endpoints/reports.py` 中新增時間檢查:
```python
from datetime import datetime, time
from app.utils.workday import is_workday, is_before_deadline
@router.post("/{report_id}/publish", response_model=PublishResponse)
def publish_report(
report_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(require_roles("admin", "editor"))
):
"""發布報告"""
# 檢查是否為工作日
if not is_workday(datetime.now().date()):
raise HTTPException(
status_code=400,
detail="非工作日無法發布報告"
)
# 檢查是否超過發布截止時間09:00
if not is_before_deadline(datetime.now(), time(9, 0)):
# 發送延遲通知
send_delay_notification(db, report)
# 但仍允許發布(根據業務需求決定)
# 或拒絕發布:
# raise HTTPException(status_code=400, detail="已超過發布截止時間09:00")
# ... 原有程式碼
```
---
## 📊 安全評分總結
### 發現的問題統計
- **高風險問題:** 5 個
- **中風險問題:** 8 個
- **低風險問題:** 4 個
- **總計:** 17 個安全問題
### 優先修復順序
1. **立即修復(上線前必須):**
- 威脅 1建立 `.gitignore`
- 威脅 3移除硬編碼密鑰
- 威脅 4確保生產環境關閉 Debug
- 威脅 5修正 CORS 設定
2. **高優先級(建議上線前修復):**
- 威脅 2建立 `.env.example`
- 威脅 6改用 logging 替代 print
- 威脅 8修復 LDAP 注入
- 威脅 9加強檔案上傳安全
3. **中優先級(上線後盡快修復):**
- 威脅 10調整 JWT 過期時間
- 威脅 12實作速率限制
- 威脅 15檢查依賴項安全
4. **低優先級(持續改進):**
- 威脅 11加強密碼強度檢查
- 威脅 13Email XSS 防護
- 威脅 14加強輸入驗證
---
## 🔧 自動化掃描建議
由於專案規模較大,建議使用自動化工具進行全面掃描:
### 1. 建立安全掃描腳本
建立 `scripts/security_scan.py`
```python
#!/usr/bin/env python3
"""
安全掃描腳本
掃描專案中的常見安全問題
"""
import re
import os
from pathlib import Path
def scan_hardcoded_secrets():
"""掃描硬編碼的密鑰和密碼"""
patterns = [
(r'password\s*=\s*["\'][^"\']+["\']', '硬編碼密碼'),
(r'api_key\s*=\s*["\'][^"\']+["\']', '硬編碼 API Key'),
(r'secret\s*=\s*["\'](?!change-me)[^"\']+["\']', '硬編碼密鑰'),
]
issues = []
for py_file in Path('app').rglob('*.py'):
content = py_file.read_text(encoding='utf-8')
for pattern, desc in patterns:
matches = re.finditer(pattern, content, re.IGNORECASE)
for match in matches:
issues.append({
'file': str(py_file),
'line': content[:match.start()].count('\n') + 1,
'issue': desc,
'code': match.group()
})
return issues
def scan_sql_injection():
"""掃描可能的 SQL 注入風險"""
issues = []
for py_file in Path('app').rglob('*.py'):
content = py_file.read_text(encoding='utf-8')
# 檢查是否有使用 f-string 或 % 格式化 SQL
if re.search(r'execute\s*\([^)]*f["\']', content) or \
re.search(r'execute\s*\([^)]*%[^)]*\)', content):
issues.append({
'file': str(py_file),
'issue': '可能的 SQL 注入風險(使用字串格式化)'
})
return issues
if __name__ == '__main__':
print("開始安全掃描...")
secrets = scan_hardcoded_secrets()
sql_issues = scan_sql_injection()
print(f"\n發現 {len(secrets)} 個硬編碼密鑰問題")
for issue in secrets:
print(f" {issue['file']}:{issue['line']} - {issue['issue']}")
print(f"\n發現 {len(sql_issues)} 個 SQL 注入風險")
for issue in sql_issues:
print(f" {issue['file']} - {issue['issue']}")
```
### 2. 使用外部安全工具
```bash
# 使用 banditPython 安全掃描工具)
pip install bandit
bandit -r app/
# 使用 safety檢查依賴項漏洞
pip install safety
safety check -r requirements.txt
# 使用 pip-audit官方推薦
pip install pip-audit
pip-audit -r requirements.txt
```
---
## ✅ 修復檢查清單
在部署到生產環境前,請確認以下項目:
- [ ] 已建立 `.gitignore` 並排除所有敏感檔案
- [ ] 已建立 `.env.example` 檔案
- [ ] 已從 Git 歷史中移除所有敏感資訊(如已提交)
- [ ] 生產環境的 `SECRET_KEY``JWT_SECRET_KEY` 已設定為強隨機值(至少 32 字元)
- [ ] 生產環境的 `DEBUG=false`
- [ ] 生產環境的 CORS 設定已明確指定允許的來源(不是 `*`
- [ ] 所有 `print()` 已替換為 `logging`
- [ ] LDAP 查詢已使用 `escape_filter_chars`
- [ ] 檔案上傳功能已加強安全檢查
- [ ] JWT Token 過期時間已調整為合理值
- [ ] 已實作速率限制(至少對登入端點)
- [ ] 已執行依賴項安全掃描並修復已知漏洞
- [ ] 已進行滲透測試
- [ ] 已設定適當的日誌監控和告警
---
## 📝 後續建議
1. **建立安全開發流程:**
- 程式碼提交前自動執行安全掃描
- 定期進行安全審計
- 建立安全事件回應流程
2. **持續監控:**
- 設定日誌監控,偵測異常行為
- 定期檢查依賴項更新和安全公告
- 監控 API 使用情況,偵測異常流量
3. **安全培訓:**
- 對開發團隊進行安全意識培訓
- 建立安全開發指南
- 定期進行安全演練
---
**報告結束**
> 本報告由資深安全架構師根據 OWASP Top 10 和業界最佳實踐編寫。建議在上線前修復所有高風險問題,並持續改進安全性。