# 每日報導 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 :.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"""

每日報導

{safe_title}

群組:{safe_group_name}
日期:{report.report_date}

摘要

{safe_summary}

閱讀完整報告

""" 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 和業界最佳實踐編寫。建議在上線前修復所有高風險問題,並持續改進安全性。