企業內部新聞彙整與分析系統 - 自動新聞抓取 (Digitimes, 經濟日報, 工商時報) - AI 智慧摘要 (OpenAI/Claude/Ollama) - 群組管理與訂閱通知 - 已清理 Python 快取檔案 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
35 KiB
每日報導 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 檔案:
# 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
檢查已提交的敏感檔案:
# 檢查是否有敏感檔案被提交
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 檔案,新開發者或部署人員無法知道需要設定哪些環境變數,可能導致:
- 遺漏重要的環境變數設定
- 使用預設值(如
secret_key: "change-me-in-production")直接上線 - 配置錯誤導致系統無法正常運作
受影響組件:
- 部署流程
- 新開發者入職
修復建議:
建立 .env.example 檔案:
# 應用程式設定
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"
如果生產環境使用這些預設值,攻擊者可以:
- 偽造 JWT Token
- 解密儲存的敏感資料
- 完全控制系統
受影響組件:
app/core/config.py第 22、44 行
駭客劇本:
我是一個攻擊者,我發現了這個應用程式。我嘗試登入,但沒有成功。沒關係,我檢查了應用程式的錯誤訊息或日誌,發現它使用 JWT 認證。我知道很多開發者會忘記更改預設的 JWT 密鑰。我下載了這個專案的原始碼(或者從公開的 Git 倉庫),看到
jwt_secret_key = "change-me"。太好了!現在我可以:
- 使用這個密鑰偽造任何用戶的 JWT Token
- 將
user_id設為管理員的 ID,role設為 "admin"- 用這個偽造的 Token 訪問所有管理功能
- 刪除所有用戶、修改所有報告、取得所有敏感資料
整個過程只需要幾分鐘,而且完全不需要破解密碼。
修復原理:
預設密鑰就像您家門的萬能鑰匙,每個人都知道。如果生產環境使用預設值,任何人都可以用這個「萬能鑰匙」進入您的系統。正確的做法是:
- 預設值應該在開發環境才使用,並且明確標註
- 生產環境必須透過環境變數提供強隨機密鑰
- 密鑰應該至少 32 字元,使用加密安全的隨機數生成器產生
- 不同環境(開發、測試、生產)應該使用不同的密鑰
修復建議與程式碼範例:
修改 app/core/config.py:
# 修改前
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 中呼叫驗證:
@asynccontextmanager
async def lifespan(app: FastAPI):
"""應用程式生命週期管理"""
# 啟動時執行
from app.core.config import validate_secrets
validate_secrets() # 新增這行
print(f"🚀 {settings.app_name} 啟動中...")
# ... 其他程式碼
產生強隨機密鑰的方法:
# 使用 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,會導致:
- 詳細的錯誤堆疊追蹤洩露給用戶
- API 文件(
/docs)公開暴露 - 效能問題
- 敏感資訊洩露
受影響組件:
app/core/config.py第 21 行app/main.py第 32-33 行(docs_url 和 redoc_url 的條件)
駭客劇本:
我訪問了這個應用程式的 API 文件頁面(
/docs),發現它完全公開,不需要認證。太好了!我可以看到所有的 API 端點、參數格式、甚至可能的錯誤回應。更糟的是,如果 Debug 模式開啟,當我故意發送錯誤的請求時,系統會返回完整的堆疊追蹤,包括:
- 檔案路徑(可能洩露伺服器結構)
- 資料庫查詢(可能洩露資料庫結構)
- 環境變數名稱(雖然不是值,但我知道系統使用什麼)
- 內部函數名稱和邏輯流程
這些資訊讓我更容易找到漏洞和攻擊點。
修復原理:
Debug 模式就像在公共場合大聲說出您的銀行帳號和密碼。它會暴露系統的內部運作細節,讓攻擊者更容易找到弱點。生產環境必須關閉 Debug 模式,並且:
- 錯誤訊息應該對用戶友好但不洩露細節
- 詳細錯誤應該記錄在伺服器日誌中,只有管理員可以查看
- API 文件應該需要認證才能訪問,或者完全關閉
修復建議與程式碼範例:
修改 app/core/config.py:
# 修改前
debug: bool = True
# 修改後
debug: bool = Field(
default=False, # 預設為 False,更安全
description="除錯模式,僅開發環境使用"
)
修改 app/main.py,加強安全檢查:
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 設定為
*,瀏覽器不會阻擋這些請求。我可以:
- 如果用戶已經登入您的系統,我可以使用他們的 Cookie 或 Token 發送請求
- 執行跨站請求偽造(CSRF)攻擊
- 竊取用戶的資料
- 在用戶不知情的情況下執行操作(如刪除報告、修改設定)
修復原理:
CORS(跨來源資源共享)就像您家門口的訪客名單。
allow_origins=["*"]意味著「任何人都可以進入」,這在開發環境可能方便,但在生產環境是災難性的。正確的做法是:
- 生產環境必須明確列出允許的來源
- 使用環境變數管理允許的來源列表
- 開發環境可以使用
*,但必須確保生產環境不會使用這個設定
修復建議與程式碼範例:
修改 app/core/config.py,新增 CORS 設定:
# 新增到 Settings 類別
cors_origins: list[str] = Field(
default=["http://localhost:3000", "http://localhost:8000"],
description="允許的 CORS 來源列表"
)
修改 app/main.py:
# 修改前
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 行(資料庫連線資訊)
修復建議:
- 使用標準 logging 模組替代 print()
建立 app/core/logging_config.py:
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()
- 修改所有使用 print() 的地方
例如,修改 app/core/security.py:
# 修改前
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
- 修改 main.py,不輸出資料庫連線資訊
# 修改前
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,但這是初始化腳本,不影響運行時安全
建議:
- 繼續使用 ORM,避免使用原始 SQL
- 如果必須使用原始 SQL,必須使用參數化查詢
- 定期進行程式碼審查,確保沒有引入原始 SQL
威脅 8:LDAP 注入風險
風險等級: Medium
威脅描述:
在 app/core/security.py 第 75 行,LDAP 搜尋過濾器直接使用用戶輸入,可能存在 LDAP 注入風險。
受影響組件:
app/core/security.py第 75 行
修復建議:
# 修改前
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 上傳功能存在以下問題:
- 檔案類型檢查僅依賴
content_type,可能被偽造 - 檔案名稱直接使用,可能存在路徑遍歷風險
- 沒有檔案大小限制
- 沒有病毒掃描
受影響組件:
app/api/v1/endpoints/settings.py第 145-171 行
修復建議:
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}
需要安裝的套件:
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 行
修復建議:
# 修改前
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:
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 中使用:
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 端點沒有速率限制,攻擊者可以:
- 對登入端點進行暴力破解攻擊
- 對 API 進行 DoS 攻擊
- 大量請求導致系統資源耗盡
修復建議:
安裝 slowapi:
pip install slowapi
在 app/main.py 中實作:
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 行
修復建議:
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 中:
# 修改前
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 中的依賴項是否有已知的安全漏洞。
修復建議:
- 使用安全掃描工具:
# 安裝 safety
pip install safety
# 掃描依賴項
safety check -r requirements.txt
- 定期更新依賴項:
# 使用 pip-audit(Python 官方推薦)
pip install pip-audit
pip-audit -r requirements.txt
- 使用 Dependabot 或類似工具自動檢查
🟢 第三部分:業務邏輯漏洞
威脅 16:AD 用戶自動建立可能導致權限提升
風險等級: Medium
威脅描述:
在 app/api/v1/endpoints/auth.py 第 84-103 行,首次 AD 登入時自動建立用戶,並預設分配「讀者」角色。如果攻擊者能夠偽造 AD 認證回應,可能自動建立帳號。
受影響組件:
app/api/v1/endpoints/auth.py第 84-103 行
修復建議:
- 新增管理員審核機制:
# 修改後:首次 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="帳號已建立,等待管理員審核啟用"
)
- 或使用白名單機制:
# 檢查用戶是否在白名單中
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 中新增時間檢查:
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:建立
.gitignore - 威脅 3:移除硬編碼密鑰
- 威脅 4:確保生產環境關閉 Debug
- 威脅 5:修正 CORS 設定
- 威脅 1:建立
-
高優先級(建議上線前修復):
- 威脅 2:建立
.env.example - 威脅 6:改用 logging 替代 print
- 威脅 8:修復 LDAP 注入
- 威脅 9:加強檔案上傳安全
- 威脅 2:建立
-
中優先級(上線後盡快修復):
- 威脅 10:調整 JWT 過期時間
- 威脅 12:實作速率限制
- 威脅 15:檢查依賴項安全
-
低優先級(持續改進):
- 威脅 11:加強密碼強度檢查
- 威脅 13:Email XSS 防護
- 威脅 14:加強輸入驗證
🔧 自動化掃描建議
由於專案規模較大,建議使用自動化工具進行全面掃描:
1. 建立安全掃描腳本
建立 scripts/security_scan.py:
#!/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. 使用外部安全工具
# 使用 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 過期時間已調整為合理值
- 已實作速率限制(至少對登入端點)
- 已執行依賴項安全掃描並修復已知漏洞
- 已進行滲透測試
- 已設定適當的日誌監控和告警
📝 後續建議
-
建立安全開發流程:
- 程式碼提交前自動執行安全掃描
- 定期進行安全審計
- 建立安全事件回應流程
-
持續監控:
- 設定日誌監控,偵測異常行為
- 定期檢查依賴項更新和安全公告
- 監控 API 使用情況,偵測異常流量
-
安全培訓:
- 對開發團隊進行安全意識培訓
- 建立安全開發指南
- 定期進行安全演練
報告結束
本報告由資深安全架構師根據 OWASP Top 10 和業界最佳實踐編寫。建議在上線前修復所有高風險問題,並持續改進安全性。