企業內部新聞彙整與分析系統 - 自動新聞抓取 (Digitimes, 經濟日報, 工商時報) - AI 智慧摘要 (OpenAI/Claude/Ollama) - 群組管理與訂閱通知 - 已清理 Python 快取檔案 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1177 lines
35 KiB
Markdown
1177 lines
35 KiB
Markdown
# 每日報導 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)}")
|
||
```
|
||
|
||
---
|
||
|
||
### 威脅 4:Debug 模式在生產環境可能啟用
|
||
**風險等級:** `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 文件,應該加上認證
|
||
# 或者使用不同的路徑和認證中間件
|
||
```
|
||
|
||
---
|
||
|
||
### 威脅 5:CORS 設定過於寬鬆(開發模式)
|
||
**風險等級:** `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)
|
||
|
||
### 威脅 7:SQL 注入風險(低風險,但需注意)
|
||
**風險等級:** `Low`(已使用 ORM,風險較低)
|
||
|
||
**威脅描述:**
|
||
專案使用 SQLAlchemy ORM,大部分查詢都是安全的。但需要檢查是否有使用原始 SQL 查詢的地方。
|
||
|
||
**檢查結果:**
|
||
✅ 未發現直接使用原始 SQL 查詢的程式碼
|
||
✅ 所有資料庫操作都透過 SQLAlchemy ORM
|
||
⚠️ 在 `scripts/init.sql` 中有原始 SQL,但這是初始化腳本,不影響運行時安全
|
||
|
||
**建議:**
|
||
1. 繼續使用 ORM,避免使用原始 SQL
|
||
2. 如果必須使用原始 SQL,必須使用參數化查詢
|
||
3. 定期進行程式碼審查,確保沒有引入原始 SQL
|
||
|
||
---
|
||
|
||
### 威脅 8:LDAP 注入風險
|
||
**風險等級:** `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
|
||
```
|
||
|
||
---
|
||
|
||
### 威脅 10:JWT 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)):
|
||
# ... 原有程式碼
|
||
```
|
||
|
||
---
|
||
|
||
### 威脅 13:Email 內容未進行 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-audit(Python 官方推薦)
|
||
pip install pip-audit
|
||
pip-audit -r requirements.txt
|
||
```
|
||
|
||
3. **使用 Dependabot 或類似工具自動檢查**
|
||
|
||
---
|
||
|
||
## 🟢 第三部分:業務邏輯漏洞
|
||
|
||
### 威脅 16:AD 用戶自動建立可能導致權限提升
|
||
**風險等級:** `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:加強密碼強度檢查
|
||
- 威脅 13:Email 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
|
||
# 使用 bandit(Python 安全掃描工具)
|
||
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 和業界最佳實踐編寫。建議在上線前修復所有高風險問題,並持續改進安全性。
|
||
|