OWASP Top 10 - 敏感資料洩露防護

OWASP Top 10 - Sensitive Data Exposure Prevention

什麼是敏感資料洩露

敏感資料洩露(Sensitive Data Exposure)是 OWASP Top 10 中的重要安全風險之一。當應用程式未能妥善保護敏感資料時,攻擊者可能竊取或修改這些資料,進而導致信用卡詐欺、身份盜用或其他犯罪行為。敏感資料需要額外的保護措施,例如在傳輸和儲存時進行加密,以及與瀏覽器交換時採取特殊預防措施。

常見的敏感資料類型

在保護資料之前,首先需要識別哪些資料屬於敏感資料:

類別範例
個人識別資訊 (PII)身分證字號、護照號碼、出生日期、地址
財務資訊信用卡號碼、銀行帳戶、交易記錄
認證資訊密碼、API 金鑰、Session Token
健康資訊病歷、診斷結果、處方資訊
商業機密原始碼、商業計畫、客戶名單
通訊內容電子郵件、聊天記錄、通話內容

常見漏洞場景

1. 明文傳輸敏感資料

1
2
3
4
5
# 不安全:使用 HTTP 傳輸登入憑證
POST http://example.com/login
Content-Type: application/x-www-form-urlencoded

username=admin&password=secret123

攻擊者可透過中間人攻擊(MITM)攔截明文傳輸的資料。

2. 不安全的資料儲存

1
2
3
4
5
6
# 不安全:明文儲存密碼
def save_user(username, password):
    cursor.execute(
        "INSERT INTO users (username, password) VALUES (?, ?)",
        (username, password)  # 直接儲存明文密碼
    )

3. 弱加密演算法

1
2
3
4
5
# 不安全:使用已被破解的 MD5 雜湊
import hashlib

def hash_password(password):
    return hashlib.md5(password.encode()).hexdigest()

4. 硬編碼金鑰

1
2
3
# 不安全:在程式碼中硬編碼加密金鑰
SECRET_KEY = "my-super-secret-key-12345"
API_KEY = "sk-1234567890abcdef"

傳輸中的資料保護 (TLS)

強制使用 HTTPS

Nginx 設定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    # 使用強加密套件
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers off;

    # 啟用 HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;
}

HSTS (HTTP Strict Transport Security)

1
2
3
4
5
6
7
# Flask 設定 HSTS
from flask import Flask
from flask_talisman import Talisman

app = Flask(__name__)
Talisman(app, force_https=True, strict_transport_security=True,
         strict_transport_security_max_age=31536000)

憑證驗證

1
2
3
4
5
6
7
import requests

# 正確:驗證 SSL 憑證
response = requests.get('https://api.example.com/data', verify=True)

# 不安全:跳過憑證驗證(僅限測試環境)
# response = requests.get('https://api.example.com/data', verify=False)

儲存中的資料保護

對稱加密 (AES-256-GCM)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

class DataEncryptor:
    def __init__(self, key=None):
        # 產生或使用提供的 256 位元金鑰
        self.key = key or AESGCM.generate_key(bit_length=256)
        self.aesgcm = AESGCM(self.key)

    def encrypt(self, plaintext):
        """加密資料"""
        # 產生隨機 96 位元 nonce
        nonce = os.urandom(12)
        ciphertext = self.aesgcm.encrypt(nonce, plaintext.encode(), None)
        # 將 nonce 與密文一起儲存
        return nonce + ciphertext

    def decrypt(self, encrypted_data):
        """解密資料"""
        # 分離 nonce 和密文
        nonce = encrypted_data[:12]
        ciphertext = encrypted_data[12:]
        plaintext = self.aesgcm.decrypt(nonce, ciphertext, None)
        return plaintext.decode()

# 使用範例
encryptor = DataEncryptor()
sensitive_data = "信用卡號碼: 4111-1111-1111-1111"
encrypted = encryptor.encrypt(sensitive_data)
decrypted = encryptor.decrypt(encrypted)

金鑰管理最佳實踐

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import os
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential

class SecureKeyManager:
    """使用雲端金鑰管理服務"""

    def __init__(self, vault_url):
        credential = DefaultAzureCredential()
        self.client = SecretClient(vault_url=vault_url, credential=credential)

    def get_encryption_key(self, key_name):
        """從金鑰保管庫取得金鑰"""
        secret = self.client.get_secret(key_name)
        return secret.value

    def rotate_key(self, key_name):
        """輪換金鑰"""
        new_key = os.urandom(32).hex()
        self.client.set_secret(key_name, new_key)
        return new_key

密碼儲存最佳實踐

使用 bcrypt

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import bcrypt

def hash_password_bcrypt(password):
    """使用 bcrypt 雜湊密碼"""
    # work factor 設為 12 (2^12 次迭代)
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
    return hashed.decode('utf-8')

def verify_password_bcrypt(password, hashed):
    """驗證 bcrypt 雜湊的密碼"""
    return bcrypt.checkpw(password.encode('utf-8'), hashed.encode('utf-8'))

# 使用範例
password = "MySecurePassword123!"
hashed = hash_password_bcrypt(password)
print(f"雜湊結果: {hashed}")
# 輸出: $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4aLQHkMKQgPK2Kbe

is_valid = verify_password_bcrypt(password, hashed)
print(f"密碼驗證: {is_valid}")  # True

使用 Argon2 (推薦)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

class SecurePasswordManager:
    def __init__(self):
        # 設定 Argon2id 參數
        self.ph = PasswordHasher(
            time_cost=3,        # 迭代次數
            memory_cost=65536,  # 記憶體使用量 (64 MB)
            parallelism=4,      # 平行處理數
            hash_len=32,        # 雜湊長度
            salt_len=16         # 鹽值長度
        )

    def hash_password(self, password):
        """使用 Argon2id 雜湊密碼"""
        return self.ph.hash(password)

    def verify_password(self, password, hashed):
        """驗證密碼"""
        try:
            self.ph.verify(hashed, password)
            return True
        except VerifyMismatchError:
            return False

    def needs_rehash(self, hashed):
        """檢查是否需要重新雜湊(參數更新時)"""
        return self.ph.check_needs_rehash(hashed)

# 使用範例
pm = SecurePasswordManager()
hashed = pm.hash_password("MySecurePassword123!")
print(f"Argon2 雜湊: {hashed}")
# 輸出: $argon2id$v=19$m=65536,t=3,p=4$randomsalt$hashedvalue

日誌與錯誤訊息處理

敏感資料遮罩

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import re
import logging

class SensitiveDataFilter(logging.Filter):
    """過濾日誌中的敏感資料"""

    PATTERNS = [
        (r'\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b', '[CREDIT_CARD]'),
        (r'\b[A-Z]\d{9}\b', '[ID_NUMBER]'),
        (r'password["\']?\s*[:=]\s*["\']?[^"\'&\s]+', 'password=[REDACTED]'),
        (r'api[_-]?key["\']?\s*[:=]\s*["\']?[\w-]+', 'api_key=[REDACTED]'),
        (r'\b[\w.-]+@[\w.-]+\.\w+\b', '[EMAIL]'),
    ]

    def filter(self, record):
        message = record.getMessage()
        for pattern, replacement in self.PATTERNS:
            message = re.sub(pattern, replacement, message, flags=re.IGNORECASE)
        record.msg = message
        record.args = ()
        return True

# 設定 logger
logger = logging.getLogger(__name__)
logger.addFilter(SensitiveDataFilter())

# 測試
logger.info("用戶信用卡: 4111-1111-1111-1111")  # 輸出: 用戶信用卡: [CREDIT_CARD]
logger.info("password=secret123")               # 輸出: password=[REDACTED]

安全的錯誤處理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from flask import Flask, jsonify
import traceback
import logging

app = Flask(__name__)
logger = logging.getLogger(__name__)

class APIError(Exception):
    """自訂 API 錯誤"""
    def __init__(self, message, status_code=400, internal_message=None):
        self.message = message
        self.status_code = status_code
        self.internal_message = internal_message or message

@app.errorhandler(APIError)
def handle_api_error(error):
    # 記錄詳細錯誤(內部使用)
    logger.error(f"API Error: {error.internal_message}")
    # 回傳給用戶的訊息(不含敏感資訊)
    return jsonify({"error": error.message}), error.status_code

@app.errorhandler(Exception)
def handle_exception(error):
    # 記錄完整堆疊追蹤(內部使用)
    logger.error(f"Unhandled Exception: {traceback.format_exc()}")
    # 回傳通用錯誤訊息(不洩漏系統資訊)
    return jsonify({"error": "發生內部錯誤,請稍後再試"}), 500

API 回應資料過濾

資料傳輸物件 (DTO) 模式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime

# 資料庫模型(包含所有欄位)
class UserDB:
    def __init__(self):
        self.id = 1
        self.username = "john_doe"
        self.email = "john@example.com"
        self.password_hash = "$argon2id$v=19$..."
        self.credit_card = "4111-1111-1111-1111"
        self.ssn = "A123456789"
        self.created_at = datetime.now()
        self.internal_notes = "VIP 客戶"

# API 回應模型(僅包含可公開的欄位)
class UserResponse(BaseModel):
    id: int
    username: str
    email: str = Field(exclude=True)  # 依需求決定是否排除
    created_at: datetime

    class Config:
        # 禁止額外欄位
        extra = "forbid"

def get_user_safe(user_db: UserDB) -> dict:
    """安全地轉換使用者資料"""
    return UserResponse(
        id=user_db.id,
        username=user_db.username,
        email=user_db.email,
        created_at=user_db.created_at
    ).model_dump(exclude_unset=True)

GraphQL 欄位層級授權

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import strawberry
from functools import wraps

def require_permission(permission):
    """欄位層級權限裝飾器"""
    def decorator(func):
        @wraps(func)
        def wrapper(self, info, *args, **kwargs):
            user = info.context.get("user")
            if not user or permission not in user.permissions:
                return None  # 或拋出授權錯誤
            return func(self, info, *args, **kwargs)
        return wrapper
    return decorator

@strawberry.type
class User:
    id: int
    username: str

    @strawberry.field
    @require_permission("view_email")
    def email(self, info) -> str:
        return self._email

    @strawberry.field
    @require_permission("view_financial")
    def credit_card_last_four(self, info) -> str:
        return self._credit_card[-4:]

測試方法

1. 傳輸加密測試

1
2
3
4
5
6
7
8
# 測試 TLS 版本和加密套件
nmap --script ssl-enum-ciphers -p 443 target.com

# 使用 testssl.sh 進行完整測試
./testssl.sh https://target.com

# 檢查憑證資訊
openssl s_client -connect target.com:443 -servername target.com

2. 敏感資料洩漏掃描

1
2
3
4
5
6
7
8
# 使用 truffleHog 掃描 Git 歷史記錄中的敏感資料
trufflehog git https://github.com/example/repo

# 使用 gitleaks 掃描
gitleaks detect --source=/path/to/repo

# 搜尋常見的敏感資料模式
grep -rn "password\|api_key\|secret" --include="*.py" /path/to/project

3. API 回應檢查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import requests

def test_api_data_exposure():
    """測試 API 是否洩漏敏感資料"""
    response = requests.get("https://api.example.com/users/1")
    data = response.json()

    sensitive_fields = [
        'password', 'password_hash', 'ssn', 'credit_card',
        'api_key', 'secret', 'token', 'internal_notes'
    ]

    exposed = [field for field in sensitive_fields if field in data]
    if exposed:
        print(f"警告: API 回應包含敏感欄位: {exposed}")
    else:
        print("通過: 未發現敏感欄位洩漏")

測試清單

  • 確認所有敏感資料傳輸使用 TLS 1.2 或以上版本
  • 確認密碼使用 bcrypt 或 Argon2 儲存
  • 確認加密金鑰不在程式碼中硬編碼
  • 確認日誌不記錄敏感資料
  • 確認錯誤訊息不洩漏系統資訊
  • 確認 API 回應不包含不必要的敏感欄位
  • 確認已啟用 HSTS
  • 確認 Cookie 設定 Secure 和 HttpOnly 屬性
  • 確認備份資料已加密
  • 確認舊版加密演算法已停用(MD5、SHA1、DES)

參考資料

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy