ecdsa (PyPI) におけるサービス拒否 (Denial of Service) の脆弱性
私は python-ecdsa において、Moderate(中程度) の深刻度の脆弱性を特定し、責任ある開示(responsible disclosure)を行いました。python-ecdsa は、先月だけで 4780 万回ダウンロード された広く使われている Python 暗号ライブラリです。
私は python-ecdsa をレビューしているときに、非常に具体的な問いを念頭に置いてこの問題を発見しました。
不正な DER が長さについて嘘をつき、パーサーがそれを信じすぎてしまうとどうなるのか?
この場合、その問いは実際のバグにたどり着きました。
ecdsa.der の DER パース用ヘルパーは、エンコードされた長さが実際に存在するバイト数よりも多くのバイト数を主張する場合に、切り詰められた(トランケートされた)データを受け入れていました。このような不正な入力は即座に拒否されるべきでした。しかし実際には、パースロジックのより深い部分まで通過し、最終的に鍵パース中に内部の IndexError を引き起こす可能性がありました。
この問題は CVE-2026-33936 として登録されました。
プロジェクト: python-ecdsa on GitHub
パッケージ: ecdsa (pip)
CVE: CVE-2026-33936
攻撃者が制御する不正な DER → 切り詰められた長さが有効として受け入れられる → パーサーが信頼境界を越えて処理を継続 → SigningKey.from_der() が内部例外パスに到達 → 予期しない IndexError / アプリケーションレベルの DoS リスク
python-ecdsa は、楕円曲線暗号のための広く使われている Python ライブラリです。
その他の機能の中でも、以下を処理します:
つまり、そのパースコードはセキュリティ境界上に直接位置しています。
ライブラリが外部から提供された鍵素材や構造化されたバイナリ入力を受け入れる場合、正確性は単なる品質の問題ではありません。 それはセキュリティ上の特性です。
拒否されるべき不正な入力が受け入れられると、後段のコードは不正な状態の上に仮定を置き始めます。 そこでバグは「単なるパースの誤り」ではなくなり、脆弱性になり始めます。
DER パースは、小さな検証ミスが過大な影響を持つ可能性がある領域の1つです。
バグの分類は単純明快です:
これはまさに、セキュリティレビューにおいて確認する価値のある種類の境界の失敗です。
私はここで奇妙な暗号挙動を探していたわけではありません。 構造化入力の処理における信頼の失敗を探していました。
それが正しい調査対象でした。
根本的な問題は、不正なまたは切り詰められた入力をパースする際の DER 長さフィールドの検証不備 でした。
具体的には、ecdsa.der.remove_octet_string() は、宣言された DER 長さがバッファ内で実際に利用可能なバイト数を超える入力を受理していました。
つまり、以下のような不正な DER を拒否する代わりに:
40963ヘルパーはそれを受け入れ、切り詰められた内容をあたかも有効であるかのように返していました。
これはすでにバグです。
しかし、より強い影響は後段で現れました。
境界で拒否される代わりに不正な入力が受理されたため、SigningKey.from_der() は後で内部例外パスに到達し、以下を送出する可能性がありました:
IndexError: index out of bounds on dimension 1
これは、呼び出し側が不正な入力に対して期待する種類の失敗ではないため重要です。
正しい動作は、UnexpectedDER や ValueError などのクリーンなパース拒否です。
つまり、脆弱性は単独で「IndexError が存在する」ということではありませんでした。
本当の脆弱性は次のとおりです:
これは1つのバグチェーンであり、無関係な2つの問題ではありません。
パーサーが不正な入力を拒否することは、見た目の改善ではありません。 それはセキュリティモデルの一部です。
ここでの重要な区別は、入力が無効だったかどうかではありません。 もちろん無効でした。
重要な区別は、ライブラリが無効な入力に対してどのように振る舞ったか です。
以下の間には実際の違いがあります:
前者は堅牢な動作です。
後者は、ソフトウェアが信頼できない DER をパースし、ライブラリの失敗が想定された例外タイプの範囲内にとどまると仮定する場合、アプリケーションレベルのリスクを生み出します。
そのため、これは単なるパーサー品質のバグではなく、脆弱性として適切に分類されました。
同じバグチェーンの異なる2つの部分を実証するために、2つの PoC を使用しました。
最初の PoC は、remove_octet_string() が、宣言された長さが利用可能なバッファを超える切り詰められた DER を受理することを示しました。
これにより、コアとなる検証の失敗が確認できました:
2番目の PoC は、より重要な後段の影響を示しました:
修正前は、SigningKey.from_der() に供給された不正な DER が、決定論的に内部の IndexError を引き起こしていました。
これにより、セキュリティ上関連する影響が確認できました:
これは「パーサーが奇妙なバイトを受け入れた」よりもはるかに強い結果です。
境界の失敗と実際の運用上の影響の両方を示しています。
最初の PoC は根本原因を証明します。
2番目の PoC は影響を証明します。
その分割は重要です。
多くの報告書は次の時点で止まります:
「このパーサーは不正なデータを受け入れる。」
それは有用ですが、なぜそのバグが重要かを示すには必ずしも十分ではありません。
このケースでは、より強力な報告は次のとおりでした:
これにより、セキュリティ上のストーリーがはるかに明確になります。
修正は最小限かつ正確でした。
パッチは、remove_sequence() ですでに使用されている、欠落していた同じ安全性ルールを追加しました:
宣言された長さは利用可能なバッファ内に収まらなければならない
そのチェックは以下に適用されました:
remove_constructed()remove_implicit()remove_octet_string()これらの境界チェックが追加されると、不正な/切り詰められた DER は即座に拒否されました:
UnexpectedDER: Length longer than the provided buffer
そして、以前 IndexError を引き起こしていた PoC は、もはや内部例外パスに到達しませんでした。
パース中にクリーンに失敗します。これはまさに最初から起こるべきだったことです。
これはパーサー脆弱性の修正として理想的なものです:
再設計なし。 曖昧さなし。 欠落していた場所に正しい検証を追加しただけです。
また、この正確な種類の不正な DER が拒否され続けることを保証するために、焦点を絞ったリグレッションテストも追加しました。
新しいテストは、以下の切り詰められた長さの拒否をカバーしています:
remove_octet_stringremove_constructedremove_implicitこれは重要でした。なぜなら、このバグは1つの奇妙なランタイムパスに関するものではなく、関連する DER ヘルパー全体で一貫して成立する必要がある検証ルールに関するものだったからです。
修正とテストの追加後、完全なテストスイートがローカルでパスしました:
python -m pytest -q
# 2018 passed, 5 skipped
これは実際の開示作業において重要です。
境界を固定するテストが付属している修正は、はるかに強力です。
この問題は妥当に Moderate(中程度) に分類されました。
ここでの主な影響は、機密性や完全性ではなく、可用性/堅牢性です。
アドバイザリの分類は次のとおりでした:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:Lこれは理にかなっています。
この主張は、不正な DER が攻撃者にコード実行を許すというものではありません。 この主張は、このライブラリを使用して信頼できない DER 素材をパースするソフトウェアにおいて、不正な DER が予期しない内部例外を引き起こす可能性があるというものです。
これは現実的で、擁護可能な DoS 型パースバグです。
この問題は、GitHub Security Advisories を通じて非公開で報告されました。
報告には以下が含まれていました:
IndexError 再現コードメンテナーはこの問題を検証し、ユニットテストとともに修正を導入するよう要請し、調整された修正は GHSA の一時的なプライベートフォークワークフローを通じて進行しました。
CVE 処理の間、GitHub はアドバイザリのテキストが複数の脆弱性を説明している可能性があるように見えたため、当初割り当てを拒否しました。 明確化は単純でした:
これは単一の根本原因(不適切な DER 長さ検証)を持つ 単一の脆弱性 であり、SigningKey.from_der() の IndexError は、同じ不正入力の受理の後段の結果であり、独立して修正可能な別の問題ではない、ということでした。
その明確化で十分であり、問題には以下が割り当てられました:
CVE-2026-33936
ここでの教訓は「DER は厄介だ」ということではありません。
DER が厄介なことは誰もがすでに知っています。
本当の教訓は次のとおりです:
不正な構造化入力は、パーサーがそれが無効であると認識した正確な時点で拒否されなければならない。
その境界を逃すと、後段のコードはもはや信頼できない仮定に基づいて動作することになります。
それが、低レベルのパースミスがセキュリティ問題になる方法です。
このバグはまた、報告書(writeup)とトリアージに関する重要なことを強化します:
「不正な入力が受理された」はストーリーの始まりでした。
「不正な入力が受理され、その後、鍵パース中に内部例外パスへ伝播した」が完全なストーリーでした。
その区別が、この件を明確かつ正確に説明するのに役立ちました。
この脆弱性は、特殊な暗号に関するものではありませんでした。
それは、パーサーが不正な入力を必要以上に長く信頼していたことに関するものでした。
切り詰められた DER 長さフィールドが境界を越え、拒否されるべきだったのに検証を通過し、最終的に鍵パースでクラッシュを伴う動作を引き起こしました。
そのため、これが CVE-2026-33936 になりました。
影響を受けるヘルパーパーサーに適切な DER 長さの境界チェックを適用することで修正されました。
