🐙

Python/CVE-2025-4516 について

に公開

CVE-2025-4516 の件、PoC

FreeBSD pkg audit で 2025/05/28 現在でも解決されていない問題がありましたので記録しておきます。


CVE-2025-4516は、PythonのCPythonにおけるUse-After-Free(解放後使用)の脆弱性です。この問題はbytes.decode("unicode_escape", error="ignore|replace")を使用した際に発生し、use-after-freeの脆弱性によってクラッシュが引き起こされる可能性があります。

技術的な詳細

エラーハンドラーが使用される際、UnicodeDecodeErrorのオブジェクト属性として設定するために新しいbytesオブジェクトが作成され、そのbytesオブジェクトが元のデータを置き換えます。その一時的なbytesオブジェクトが破棄された後、デコードされたデータへのポインタが無効になってしまいます。
Use-after-free in unicode_escape decoder with error handler · Issue #133767 · python/cpython

影響を受けるバージョン:

Python 3.9 < 3.9.22_1
Python 3.10 < 3.10.17_1
Python 3.11 < 3.11.12_1
Python 3.12 < 3.12.10_1

問題の条件

この脆弱性は以下の条件が揃った場合にのみ発生します:

unicode_escapeエンコーディングを使用
error="ignore"またはerror="replace"パラメータを指定

PoC

Claude4.0 に依頼して、PoCを作成しました。

#!/usr/bin/env python3
"""
CVE-2025-4516 の概念実証(PoC)と回避策のデモ

この脆弱性は bytes.decode("unicode_escape", error="ignore|replace") 
を使用した際のuse-after-freeの問題です。
"""

# 脆弱なコード例(使用しないでください)
def vulnerable_example():
    """
    脆弱性を持つコード例
    注意: このコードは実際の環境でクラッシュを引き起こす可能性があります
    """
    print("=== 脆弱なコード例 ===")
    
    # 不正なunicode escapeシーケンスを含むバイト列
    malicious_bytes = b'\\u41\\u42\\u43invalid_escape\\u44'
    
    try:
        # この呼び出しがuse-after-freeを引き起こす可能性がある
        result = malicious_bytes.decode("unicode_escape", errors="ignore")
        print(f"結果: {result}")
    except Exception as e:
        print(f"エラー: {e}")
    
    try:
        # replaceでも同様の問題が発生する可能性
        result = malicious_bytes.decode("unicode_escape", errors="replace")
        print(f"結果: {result}")
    except Exception as e:
        print(f"エラー: {e}")


def safe_workaround():
    """
    推奨される回避策
    try-exceptでUnicodeDecodeErrorをキャッチする方法
    """
    print("\n=== 安全な回避策 ===")
    
    malicious_bytes = b'\\u41\\u42\\u43invalid_escape\\u44'
    
    try:
        # errorパラメータを使用せず、例外をキャッチ
        result = malicious_bytes.decode("unicode_escape")
        print(f"成功: {result}")
    except UnicodeDecodeError as e:
        print(f"UnicodeDecodeError をキャッチ: {e}")
        
        # エラーを無視したい場合の処理
        try:
            # 有効な部分のみを処理
            valid_part = malicious_bytes[:e.start]
            result = valid_part.decode("unicode_escape")
            print(f"有効な部分の結果: {result}")
        except UnicodeDecodeError:
            print("完全に無効なデータです")
    
    # または、異なるエンコーディングを使用
    try:
        # latin-1は常に成功する(バイト値をそのままUnicodeに変換)
        fallback_result = malicious_bytes.decode("latin-1")
        print(f"フォールバック結果: {repr(fallback_result)}")
    except Exception as e:
        print(f"フォールバック失敗: {e}")


def safe_unicode_escape_decoder():
    """
    より堅牢なunicode_escapeデコーダーの実装例
    """
    print("\n=== 堅牢なデコーダー実装 ===")
    
    def safe_unicode_escape_decode(data, errors='strict'):
        """
        安全なunicode_escapeデコーダー
        """
        if isinstance(data, str):
            data = data.encode('utf-8')
        
        try:
            return data.decode('unicode_escape'), len(data)
        except UnicodeDecodeError as e:
            if errors == 'ignore':
                # エラー部分を無視
                valid_data = data[:e.start] + data[e.end:]
                return safe_unicode_escape_decode(valid_data, errors)[0], len(data)
            elif errors == 'replace':
                # エラー部分を置換文字で置き換え
                replacement = '�'
                valid_part = data[:e.start].decode('unicode_escape', errors='strict')
                remaining_part = safe_unicode_escape_decode(data[e.end:], errors)[0]
                return valid_part + replacement + remaining_part, len(data)
            else:
                raise
    
    # テストケース
    test_cases = [
        b'\\u0041\\u0042\\u0043',  # 正常なケース: "ABC"
        b'\\u41\\u42\\u43invalid\\u44',  # 不正なエスケープを含むケース
        b'\\u0041invalid_escape\\u0042',  # 途中に不正なエスケープ
    ]
    
    for i, test_data in enumerate(test_cases, 1):
        print(f"\nテストケース {i}: {test_data}")
        
        # 安全な実装でテスト
        try:
            result_strict = safe_unicode_escape_decode(test_data, 'strict')
            print(f"  strict: {repr(result_strict[0])}")
        except UnicodeDecodeError as e:
            print(f"  strict: エラー - {e}")
        
        try:
            result_ignore = safe_unicode_escape_decode(test_data, 'ignore')
            print(f"  ignore: {repr(result_ignore[0])}")
        except Exception as e:
            print(f"  ignore: エラー - {e}")
        
        try:
            result_replace = safe_unicode_escape_decode(test_data, 'replace')
            print(f"  replace: {repr(result_replace[0])}")
        except Exception as e:
            print(f"  replace: エラー - {e}")


def check_vulnerability():
    """
    システムが脆弱性の影響を受けるかチェック
    """
    import sys
    print("\n=== 脆弱性チェック ===")
    print(f"Python バージョン: {sys.version}")
    
    # バージョンチェック
    version_info = sys.version_info
    vulnerable_versions = [
        (3, 9),   # < 3.9.22
        (3, 10),  # < 3.10.17
        (3, 11),  # < 3.11.12
        (3, 12),  # < 3.12.10
    ]
    
    is_potentially_vulnerable = False
    for major, minor in vulnerable_versions:
        if version_info.major == major and version_info.minor == minor:
            is_potentially_vulnerable = True
            break
    
    if is_potentially_vulnerable:
        print("⚠️  このPythonバージョンはCVE-2025-4516の影響を受ける可能性があります")
        print("推奨: Pythonを最新バージョンにアップデートしてください")
    else:
        print("✅ このPythonバージョンは既知の脆弱性パッチが適用されている可能性があります")


if __name__ == "__main__":
    print("CVE-2025-4516 概念実証とセキュリティ対策デモ")
    print("=" * 50)
    
    # システムの脆弱性チェック
    check_vulnerability()
    
    # 注意:実際の脆弱なコードは安全のためコメントアウト
    # vulnerable_example()  # 危険なため実行しない
    
    # 安全な回避策のデモ
    safe_workaround()
    
    # 堅牢な実装例
    safe_unicode_escape_decoder()
    
    print("\n" + "=" * 50)
    print("重要な推奨事項:")
    print("1. bytes.decode('unicode_escape', errors='ignore|replace') の使用を避ける")
    print("2. 代わりにtry-except文でUnicodeDecodeErrorをキャッチする")
    print("3. Pythonを最新の安全なバージョンにアップデートする")
    print("4. 入力データの検証を強化する")

PoC 実行の結果

=== 脆弱性チェック ===
Python バージョン: 3.11.12 (main, May  8 2025, 01:07:16) [Clang 18.1.6 (https://github.com/llvm/llvm-project.git llvmorg-18.1.6-0-g1118c2
⚠️  このPythonバージョンはCVE-2025-4516の影響を受ける可能性があります
推奨: Pythonを最新バージョンにアップデートしてください

=== 安全な回避策 ===
UnicodeDecodeError をキャッチ: 'unicodeescape' codec can't decode bytes in position 0-3: truncated \uXXXX escape
有効な部分の結果: 
フォールバック結果: '\\u41\\u42\\u43invalid_escape\\u44'

=== 堅牢なデコーダー実装 ===

テストケース 1: b'\\u0041\\u0042\\u0043'
  strict: 'ABC'
  ignore: 'ABC'
  replace: 'ABC'

テストケース 2: b'\\u41\\u42\\u43invalid\\u44'
  strict: エラー - 'unicodeescape' codec can't decode bytes in position 0-3: truncated \uXXXX escape
  ignore: 'invalid'
  replace: '���invalid�'

テストケース 3: b'\\u0041invalid_escape\\u0042'
  strict: 'Ainvalid_escapeB'
  ignore: 'Ainvalid_escapeB'
  replace: 'Ainvalid_escapeB'

==================================================
  • テストケース2 で文字が化けています。無効なポインタへのアクセスが発生しています。

githubのPR状況

  • プルリクエストの概要
    これはPython 3.11ブランチへのセキュリティ修正をバックポート(移植)するプルリクエストです。タイトルは「[3.11] gh-133767: Fix use-after-free in the unicode-escape decoder with an error handler」 [3.11] gh-133767: Fix use-after-free in the unicode-escape decoder with an error handler (GH-129648) (GH-133944) by serhiy-storchaka · Pull Request #134341 · python/cpython

  • 主な変更点

_PyUnicode_DecodeUnicodeEscapeInternal()の修正: 最初の無効なエスケープを返すための別の方法を実装
_PyBytes_DecodeEscape()の変更: 元々この問題はありませんでしたが、_PyUnicode_DecodeUnicodeEscapeInternal()との互換性のために変更

  • 影響範囲
    このプルリクエストでは**+198行の追加、-57行の削除**が行われており、比較的大規模な修正となっています。

まとめ

  • この脆弱性について公式に対応が終わっていないので、散発的なDoSが起こるでしょう。
  • 対策を急ぐ場合は、以下の回避策になるかと思われます。
  • bytes.decode('unicode_escape', errors='ignore|replace') の使用を避ける
  • 代わりにtry-except文でUnicodeDecodeErrorをキャッチする

Discussion