機密データへの弱いハッシュの使用

機密データへの弱いハッシュの使用

説明

MD5とSHA-1は、衝突耐性が必要なセキュリティ用途には適していません。パスワードの保存には、このような高速なハッシュではなく、ソルトと調整可能な計算コストを備えた専用のパスワードハッシュが必要です。また、鍵のないハッシュでダウンロードリンクやトークンを認証すると、攻撃者は内容を変更してハッシュも計算し直せます。この偽造に衝突を作る必要はありません。

想定される影響

  • 認証の回避や偽造: 検証方式によっては、衝突を利用して弱いハッシュによる完全性検証を回避されるおそれがあります。
  • パスワードの漏えい: ソルトのない高速なハッシュは、大量の候補による推測や事前計算済みの照合表を使った攻撃を容易にします。
  • 完全性の低下: 改ざんされたデータを正しいものとして受け入れる可能性があります。
  • リンクやトークンの偽造: 鍵のないハッシュは、データの改ざん後に攻撃者が再計算できます。

対処方法

  • パスワード保存: Argon2idを優先し、要件に応じてbcrypt、scrypt、PBKDF2-HMAC-SHA256を適切に設定します。ランダムなソルトと十分な計算・メモリコストを使い、導出値は hmac.compare_digest などの定数時間比較で照合します。PBKDF2-HMAC-SHA256では600,000回以上と16バイト以上のランダムなソルトを使い、サービスの処理能力に合わせてコストを調整します。
  • 完全性と認証: 真正性が必要な場合は、HMAC-SHA256などの鍵付きMACを使います。MD5とSHA-1をセキュリティ用途から除き、鍵のないダイジェストが適する用途ではSHA-256/512やSHA-3を使います。
  • 移行: ログイン時にパスワードを再ハッシュします。ハッシュとともにアルゴリズムとパラメーターを保存し、後から更新できるようにします。

例

変更前

python
import hashlib

# パスワードやトークンへの弱いMD5/SHA-1の使用

def store_password_md5(username: str, password: str) -> str:
    # BAD: ソルトなしの高速なMD5でパスワードをハッシュします
    digest = hashlib.md5(password.encode("utf-8")).hexdigest()
    return f"{username}:{digest}"

def sign_download_link_sha1(user_id: str, expires: int) -> str:
    # BAD: 鍵のないSHA-1ダイジェストではデータを認証できません
    data = f"{user_id}:{expires}".encode()
    sig = hashlib.sha1(data).hexdigest()
    return f"{user_id}:{expires}:{sig}"

変更後

python
import os
import hmac
import base64
import hashlib
import secrets

# PBKDF2-HMAC-SHA256によるパスワード保存

def hash_password(password: str) -> str:
    salt = os.urandom(16)
    iterations = 600_000
    dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
    return f"pbkdf2_sha256${iterations}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"

def verify_password(password: str, stored: str) -> bool:
    scheme, iters_s, b64_salt, b64_dk = stored.split("$")
    if scheme != "pbkdf2_sha256":
        return False
    salt = base64.b64decode(b64_salt)
    iterations = int(iters_s)
    true_dk = base64.b64decode(b64_dk)
    test_dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
    return hmac.compare_digest(true_dk, test_dk)

# HMAC-SHA256による完全性検証・認証(秘密鍵が必要)

def sign_download_link(user_id: str, expires: int, secret_key: bytes) -> str:
    # secret_keyはsecrets.token_bytes(32)などで生成し、安全に保存します
    msg = f"{user_id}:{expires}".encode()
    mac = hmac.new(secret_key, msg, hashlib.sha256).hexdigest()
    return f"{user_id}:{expires}:{mac}"

解説:

  • 変更前: ソルトのない高速なMD5はパスワードの推測を容易にします。鍵のないSHA-1のリンクハッシュは、内容を変えた後に再計算できます。
  • 変更後: PBKDF2-HMAC-SHA256を600,000回実行し、ランダムなソルトで推測のコストを高めます。hmac.compare_digest は比較時のタイミング情報の漏えいを減らします。HMAC-SHA256では、保護すべき秘密鍵を使ってリンクのデータを認証します。

参考資料