cryptographic-failures について
Python アプリの暗号化の失敗を調べるセキュリティスキルです。Bandit による静的スキャンと優先度別のコード確認、修正指針を組み合わせます。
主な機能
- 01Bandit による暗号問題のスキャン
- 02パスワードハッシュと乱数の確認
- 03ハードコード鍵と TLS 検証の確認
- 04JWT と共通鍵暗号の監査
- 05Python フレームワーク別の指針
活用シーン
- 01Python コードの弱い暗号化の監査
- 02トークンとパスワード処理の確認
- 03TLS と証明書検証の点検
- 04暗号問題の修正方針の策定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ scholarly360/ owasp-top10-web-skills --skill cryptographic-failuresインストール後、Claude Code で/cryptographic-failuresを入力して読み込まれたか確認します。
同梱ファイルreferences/owasp-a04-full.md
詳細
A04:2025 — 暗号化の失敗
OWASP ランク: #4(TLS の採用が進んだため、#2 から低下) 発生率: アプリケーションの 3.80%、160 万件以上、32 件の CWE
暗号化の失敗とは、保存中および転送中のデータを保護する暗号化の欠如、誤用、または脆弱性を指します。Python には、Java/.NET とは異なる特有の落とし穴があります。
このスキルで扱う内容
| 領域 | 主なリスク | Python 固有の落とし穴 |
|---|---|---|
| パスワードのハッシュ化 | 脆弱なアルゴリズム、または未使用 | bcrypt/Argon2 ではなく hashlib.md5/sha1 を使用 |
| 乱数 | 予測可能な値 | import secrets ではなく import random を使用 |
| ハードコードされたキー | キーの漏えい | ソースコード内に SECRET_KEY = "..." のリテラルがある |
| TLS/SSL | 証明書の検証回避 | requests/httpx で verify=False を指定 |
| JWT シークレット | キーの強度、アルゴリズム | 脆弱なシークレット、アルゴリズムの固定なし |
| 共通鍵暗号 | 非推奨のモード | ECB モード、認証付き暗号化なし |
監査の手順
1. Bandit による静的スキャン
暗号化の問題を自動的に検出する、Python 向けの主要な SAST ツールである Bandit を実行します。
pip install bandit
bandit -r . -t B303,B304,B305,B306,B311,B324,B501,B502,B503,B504,B505,B506 -f json -o bandit_crypto.json
暗号化の失敗に関連する主な Bandit テスト ID:
| Bandit ID | 検出対象 | 深刻度 |
|---|---|---|
| B303 | MD5/SHA1 の使用 | 中 |
| B304/B305 | 脆弱な暗号モード(ECB、DES) | 高 |
| B311 | セキュリティ関連の処理での random モジュール | 低 |
| B324 | hashlib での脆弱なハッシュの使用 | 中 |
| B501 | SSL コンテキストでの verify=False | 高 |
| B502/B503/B504 | 安全でない SSL/TLS バージョン | 中〜高 |
| B105/B106 | ハードコードされたパスワード/シークレット | 中 |
2. コードパターンの手動レビュー
以下のパターンを優先度の高い順に確認します。
🔴 重大 — パスワードのハッシュ化
# BAD — MD5/SHA1 for passwords: CWE-328, CWE-759, CWE-916
hashlib.md5(password.encode()).hexdigest()
hashlib.sha1(password.encode()).hexdigest()
hashlib.sha256(password.encode()).hexdigest() # also bad — no salt, no stretching
# GOOD — use passlib with bcrypt or argon2
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
# OR
pwd_context = CryptContext(schemes=["bcrypt"], bcrypt__rounds=12, deprecated="auto")
hashed = pwd_context.hash(plain_password)
確認項目:
- パスワードまたはトークンのハッシュ化に
hashlib.*の呼び出しを使用している箇所 - ソルトがない(
hashlib.pbkdf2_hmacを使わず、生のパスワードをハッシュ化している) - bcrypt の
rounds < 12— 総当たり攻撃に耐えるには ≥12 にする必要があります
🔴 重大 — 脆弱な乱数(CWE-330)
# BAD — random is NOT cryptographically secure
import random
token = random.randint(100000, 999999) # predictable OTP
reset_key = random.choice(string.ascii_letters + string.digits) # guessable
# GOOD
import secrets
token = secrets.randbelow(900000) + 100000 # secure OTP
reset_key = secrets.token_urlsafe(32) # secure reset token
session_id = secrets.token_hex(16) # secure session ID
確認項目: トークン、OTP、セッション ID、キー、またはノンスに関わるコードパスで、random.random()、random.randint()、random.choice()、random.shuffle() を使用している箇所。
🔴 高 — ハードコードされた暗号キー(CWE-321)
# BAD — key in source code
SECRET_KEY = "my-super-secret-key"
JWT_SECRET = "hardcoded_jwt_secret_123"
app.config['SECRET_KEY'] = "flask_secret"
# GOOD — load from environment
import os
SECRET_KEY = os.environ["SECRET_KEY"] # raises KeyError if missing (intentional)
SECRET_KEY = os.getenv("SECRET_KEY") # returns None silently (verify it's not None)
キーの最小長:
- Flask
SECRET_KEY:ランダムな 32 バイト以上(secrets.token_hex(32)を使用) - JWT HMAC シークレット:32 文字以上
- AES キー:128、192、または 256 ビット(16、24、または 32 バイト)
🔴 高 — TLS/SSL 証明書の検証回避(CWE-295)
# BAD — disables certificate validation entirely
requests.get(url, verify=False)
httpx.get(url, verify=False)
ssl._create_unverified_context()
urllib3.disable_warnings() # companion to verify=False
# GOOD
requests.get(url) # verify=True is the default
requests.get(url, verify="/path/to/ca-bundle.crt") # custom CA
🟡 中 — JWT の設定(CWE-327)
# BAD — algorithm confusion attacks
jwt.decode(token, key, algorithms=["none"]) # NEVER allow "none"
jwt.decode(token, key) # no algorithm pin = vulnerable
# BAD — weak secret loaded without validation
JWT_SECRET = os.getenv("JWT_SECRET", "default") # fallback default is dangerous
# GOOD
JWT_SECRET = os.environ["JWT_SECRET"] # fail hard if missing
assert len(JWT_SECRET) >= 32, "JWT secret too short"
jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) # explicit allowlist
次の点も確認します。
- トークンの有効期限(
expクレーム)— アクセストークンは 15〜30 分に設定します - デコード時に
audおよびissクレームが検証されること
🟡 中 — 非推奨の共通鍵暗号(CWE-327)
# BAD — ECB mode leaks patterns in data
from Crypto.Cipher import AES
cipher = AES.new(key, AES.MODE_ECB)
# BAD — no authentication = vulnerable to tampering
cipher = AES.new(key, AES.MODE_CBC, iv)
# GOOD — authenticated encryption
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
# ChaCha20-Poly1305 is also acceptable
FastAPI 固有の確認項目
# 1. Verify OAuth2/JWT middleware algorithm pinning
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
ALGORITHM = "HS256" # must be explicit, not a variable that could be overridden
# Check: is ALGORITHM loaded from config where it could be tampered with?
# 2. Verify password hashing in user creation/login endpoints
@router.post("/register")
async def register(user: UserCreate, db: Session = Depends(get_db)):
hashed = pwd_context.hash(user.password) # must NOT store plain or MD5
# 3. Token generation for password resets / email verification
def generate_reset_token() -> str:
return secrets.token_urlsafe(32) # GOOD
# NOT: str(uuid.uuid4()) — UUID v4 uses os.urandom, acceptable but secrets is preferred
# NOT: random-based anything
Flask 固有の確認項目
# 1. Session cookie signing uses SECRET_KEY — must be strong
app.secret_key = os.environ["SECRET_KEY"] # GOOD
app.secret_key = "dev" # BAD — common in tutorials, dangerous in prod
# 2. Flask-Login password verification
from flask_bcrypt import Bcrypt
bcrypt = Bcrypt(app)
bcrypt.generate_password_hash(password, rounds=12) # rounds≥12
# 3. HTTPS enforcement via HSTS
from flask_talisman import Talisman
Talisman(app, force_https=True) # adds HSTS header
主な CWE リファレンス
| CWE | 名称 | よくある例 |
|---|---|---|
| CWE-327 | 破損またはリスクのある暗号アルゴリズムの使用 | MD5、SHA1、ECB、DES、RC4 |
| CWE-328 | 可逆な一方向ハッシュ | パスワードへの MD5/SHA1 の使用(レインボーテーブルで元に戻せる) |
| CWE-330 | ランダム性が不十分な値 | トークン/キーに random モジュールを使用 |
| CWE-321 | ハードコードされた暗号キー | ソースコード内のシークレット |
| CWE-759 | ソルトなしの一方向ハッシュ | hashlib.sha256(password) — ソルトなし |
| CWE-916 | 計算コストが不十分なパスワードハッシュ | bcrypt rounds<12、PBKDF2 iterations<600k |
| CWE-295 | 不適切な証明書検証 | HTTP クライアントでの verify=False |
修正チェックリスト
最小限の暗号化スタック(Python 2025)
Password hashing: passlib[argon2] or passlib[bcrypt] (rounds≥12)
Token generation: secrets module exclusively
Symmetric crypto: cryptography library, AES-GCM or ChaCha20-Poly1305
JWT: python-jose or PyJWT, algorithm pinned, secret≥32 chars
TLS: verify=True always; pin CA bundle where needed
Config/secrets: os.environ (fail-hard) or a secrets manager (Vault, AWS SSM)
HSTS: flask-talisman (Flask) or custom middleware (FastAPI)
コード検索に使える正規表現パターン
# Find weak hashing
grep -rn "hashlib\.md5\|hashlib\.sha1\|hashlib\.sha224" --include="*.py"
# Find insecure random
grep -rn "import random\b" --include="*.py"
grep -rn "random\.randint\|random\.choice\|random\.random()" --include="*.py"
# Find hardcoded secrets
grep -rn "SECRET_KEY\s*=\s*['\"]" --include="*.py"
grep -rn "verify=False" --include="*.py"
# Find ECB mode
grep -rn "MODE_ECB\|AES\.MODE_ECB" --include="*.py"
関連項目
references/owasp-a04-full.md— OWASP A04:2025 の完全な仕様と拡張 CWE リスト- サプライチェーンの暗号化に関する問題(依存関係のハッシュ固定)については、
software-supply-chainスキルを参照してください - 暗号化以外の JWT 関連の認証ロジック(有効期限、クレーム)については、
authentication-failuresスキルを参照してください
コメント
Markdown 対応