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

35 KiB
Raw Permalink Blame History

每日報導 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-branchgit 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 檔案,新開發者或部署人員無法知道需要設定哪些環境變數,可能導致:

  1. 遺漏重要的環境變數設定
  2. 使用預設值(如 secret_key: "change-me-in-production")直接上線
  3. 配置錯誤導致系統無法正常運作

受影響組件:

  • 部署流程
  • 新開發者入職

修復建議:

建立 .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"

如果生產環境使用這些預設值,攻擊者可以:

  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 設為管理員的 IDrole 設為 "admin"
  3. 用這個偽造的 Token 訪問所有管理功能
  4. 刪除所有用戶、修改所有報告、取得所有敏感資料

整個過程只需要幾分鐘,而且完全不需要破解密碼。

修復原理:

預設密鑰就像您家門的萬能鑰匙,每個人都知道。如果生產環境使用預設值,任何人都可以用這個「萬能鑰匙」進入您的系統。正確的做法是:

  1. 預設值應該在開發環境才使用,並且明確標註
  2. 生產環境必須透過環境變數提供強隨機密鑰
  3. 密鑰應該至少 32 字元,使用加密安全的隨機數生成器產生
  4. 不同環境(開發、測試、生產)應該使用不同的密鑰

修復建議與程式碼範例:

修改 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)}")

威脅 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

# 修改前
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 文件,應該加上認證
    # 或者使用不同的路徑和認證中間件

威脅 5CORS 設定過於寬鬆(開發模式)

風險等級: Medium(開發環境) / High(生產環境)

威脅描述:app/main.py 第 40 行,當 debug=TrueCORS 設定為 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 設定:

# 新增到 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 行(資料庫連線資訊)

修復建議:

  1. 使用標準 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()
  1. 修改所有使用 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
  1. 修改 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

威脅 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 行

修復建議:

# 修改前
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 行

修復建議:

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

威脅 10JWT 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 端點沒有速率限制,攻擊者可以:

  1. 對登入端點進行暴力破解攻擊
  2. 對 API 進行 DoS 攻擊
  3. 大量請求導致系統資源耗盡

修復建議:

安裝 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)):
    # ... 原有程式碼

威脅 13Email 內容未進行 XSS 防護

風險等級: Low(內部系統,但仍需注意)

威脅描述:app/services/notification_service.pyEmail 內容直接使用報告標題和摘要,如果這些內容包含惡意腳本,可能在某些 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 中的依賴項是否有已知的安全漏洞。

修復建議:

  1. 使用安全掃描工具:
# 安裝 safety
pip install safety

# 掃描依賴項
safety check -r requirements.txt
  1. 定期更新依賴項:
# 使用 pip-auditPython 官方推薦)
pip install pip-audit
pip-audit -r requirements.txt
  1. 使用 Dependabot 或類似工具自動檢查

🟢 第三部分:業務邏輯漏洞

威脅 16AD 用戶自動建立可能導致權限提升

風險等級: Medium

威脅描述:app/api/v1/endpoints/auth.py 第 84-103 行,首次 AD 登入時自動建立用戶,並預設分配「讀者」角色。如果攻擊者能夠偽造 AD 認證回應,可能自動建立帳號。

受影響組件:

  • app/api/v1/endpoints/auth.py 第 84-103 行

修復建議:

  1. 新增管理員審核機制:
# 修改後:首次 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="帳號已建立,等待管理員審核啟用"
    )
  1. 或使用白名單機制:
# 檢查用戶是否在白名單中
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. 立即修復(上線前必須):

    • 威脅 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

#!/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. 使用外部安全工具

# 使用 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_KEYJWT_SECRET_KEY 已設定為強隨機值(至少 32 字元)
  • 生產環境的 DEBUG=false
  • 生產環境的 CORS 設定已明確指定允許的來源(不是 *
  • 所有 print() 已替換為 logging
  • LDAP 查詢已使用 escape_filter_chars
  • 檔案上傳功能已加強安全檢查
  • JWT Token 過期時間已調整為合理值
  • 已實作速率限制(至少對登入端點)
  • 已執行依賴項安全掃描並修復已知漏洞
  • 已進行滲透測試
  • 已設定適當的日誌監控和告警

📝 後續建議

  1. 建立安全開發流程:

    • 程式碼提交前自動執行安全掃描
    • 定期進行安全審計
    • 建立安全事件回應流程
  2. 持續監控:

    • 設定日誌監控,偵測異常行為
    • 定期檢查依賴項更新和安全公告
    • 監控 API 使用情況,偵測異常流量
  3. 安全培訓:

    • 對開發團隊進行安全意識培訓
    • 建立安全開發指南
    • 定期進行安全演練

報告結束

本報告由資深安全架構師根據 OWASP Top 10 和業界最佳實踐編寫。建議在上線前修復所有高風險問題,並持續改進安全性。