技術約17分で読めます

phpMyAdmin 5.2.3のWebAuthn二要素認証バイパスを公式配布物と検証した

いけさん目次

2026-10-29 追記: AAGUIDの生バイト保存によりセキュリティキー登録が失敗する別の不具合と修正状況を検証した → phpMyAdmin 5.2系のWebAuthnキー登録がAAGUID生バイト保存でTypeErrorになる

TL;DR

影響 phpMyAdminのWebAuthn二要素認証で、予備実装 CustomServer が使われる環境における認証バイパス。MySQLパスワード所持の攻撃者がセキュリティキーなしでログイン可能(署名を検証していないため)

条件 web-auth/webauthn-lib が入っていない環境のみ。公式サイトの5.2.3のtarballやzip、Docker公式イメージには同梱されており対象外。Gitの開発ブランチから --no-dev で依存を入れた環境やDebianパッケージが対象

状況 2026年10月29日時点でCVE番号もphpMyAdminのセキュリティ告知(PMASA)も未発行。開発ブランチにも署名検証コードは未反映

実測 2026年10月29日にループバック(127.0.0.1)のDocker検証環境で確認。CustomServer ではPoCが成功し、公式tarball構成ではTLS環境下で不正な署名を Invalid signature. で正常に拒否

確認 phpMyAdmin配下に vendor/web-auth/webauthn-lib/src/Server.php があるかどうかで稼働実装を判別可能


2026年10月下旬、phpMyAdmin 5.2.3の二要素認証を回避してデータベース全体にアクセスできるという主張と概念実証(PoC)のリポジトリが、Abraxas LabsによってGitHubで公開された(脆弱性の深刻度スコアはCVSS 8.1 Highの自己評価)。
PoCは既定の5.2.3配布物も対象になると説明していた。

phpMyAdminはMySQLやMariaDBをブラウザから管理するためのWebアプリケーションだ。サーバーに導入して使うケースが多いため、手元の環境で配布物とソースコードを確認した。
署名を検証しない欠陥そのものはコード上に存在していた。
ただ、公式サイトからダウンロードしたtarball(圧縮アーカイブ)を展開すると、PoCが前提としている構成と合わない部分が見つかった。
コードを読むだけでは実際の挙動が確定しないため、2026年10月29日に127.0.0.1限定のDocker検証環境を構築し、PoCを両方の構成で動かして確かめた。

phpMyAdminのWebAuthn二要素認証の作り

phpMyAdminは4.8.0から二要素認証に対応している。
使うにはphpMyAdmin環境保管領域(設定やブックマークを保存する専用のデータベース)の設定が必要で、そのうえで各ユーザーが設定画面から自分で有効にする。
方式は認証アプリのワンタイムコードと、セキュリティキーの2系統がある。
5.2.3の設定画面では、セキュリティキー側が「Hardware Security Key (WebAuthn/FIDO2)」として表示される。

WebAuthnは、ブラウザとYubiKeyのようなセキュリティキー(認証器)を使ってログインするための規格だ。
登録時に認証器が鍵ペアを作り、サーバーは公開鍵だけを保存する。
ログイン時はサーバーが毎回違うランダムな値(チャレンジ)を送り、認証器がそれを含むデータに秘密鍵で署名して返す。
サーバーは保存しておいた公開鍵でその署名を検証する。
秘密鍵はサーバーに渡らないので、署名が正しければ「その秘密鍵を持つ認証器でいま操作した」と判断できる。
W3CのWebAuthn仕様でも、ログイン時の検証手順に公開鍵による署名検証が含まれている。

phpMyAdminの検証処理には、外部ライブラリ web-auth/webauthn-lib を使う WebauthnLibServer と、自前実装の CustomServer の2種類がある。
どちらを使うかの切り替えコードは、libraries/classes/Plugins/TwoFactor/WebAuthn.php の次の1行。

private function createServer(): Server
{
    return class_exists(WebauthnServer::class) ? new WebauthnLibServer($this->twofactor) : new CustomServer();
}

ライブラリのクラス Webauthn\Server が読み込めればライブラリ版、なければ自前の CustomServer になる。
composer.json では、web-auth/webauthn-lib は推奨(suggest)と開発用(require-dev)にのみ指定されている。

CustomServerが確かめていること

問題の処理は CustomServer::parseAndValidateAssertionResponse だ。
ログイン時にブラウザから届いた応答を、次の順で確かめている。

確認項目内容
認証器のID応答の認証器IDが、そのユーザーが登録したIDの一覧に含まれるか
種類clientDataJSON の type が webauthn.get(ログイン用)か
チャレンジclientDataJSON のチャレンジが、セッションに保存した値と一致するか
オリジンclientDataJSON のオリジン(ホスト名)が、アクセス先のホスト名と一致するか
rpIdHashauthenticatorData の先頭32バイトが、ホスト名のSHA-256と一致するか
User Presence(UP)フラグauthenticatorData のフラグで、利用者が認証器に触れたことを示すビット(UP)が立っているか

このほかに、JSONに必要な項目がそろっているか、id と rawId が一致するか、各値をBase64としてデコードできるかといった形式の確認がある。
signature フィールドも、存在して空でないこととBase64としてデコードできることだけを確認している。
登録時に保存した公開鍵で署名を検証する処理はどこにもない。
表の6項目は、どれも攻撃者が自分で組み立てられる値だ。
clientDataJSON はJSONをBase64にしただけで、authenticatorData もホスト名のハッシュ、フラグ、署名回数を並べたバイト列なので、秘密鍵がなくても作れる。

phpMyAdmin自身の単体テスト CustomServerTest も、公開鍵を一切指定せずにこの関数を呼んで、成功することを確認している。
PoCのREADMEはこの点を、署名を検証していない裏付けとして挙げている。

同じ関数は5.2.1と5.2.2のタグでも全く同じだった。
PoCの対象は5.2.3のみだが、CustomServer が動く環境であれば5.2.1以降でも署名は検証されない。

攻撃の流れ

PoCによる認証回避の手順は次の流れになる。
前提として、攻撃者はMySQLのユーザー名とパスワードをすでに持っている。

flowchart TD
  A["MySQLのユーザー名とパスワードで<br/>phpMyAdminにログイン"] --> B["二要素認証の画面が出る"]
  B --> C["画面のHTMLから<br/>チャレンジを取得する"]
  C --> D["clientDataJSONと<br/>authenticatorDataを自分で組み立てる"]
  D --> E["署名欄にでたらめな値を入れて送信"]
  E --> F["CustomServerは形式と6項目を確認し<br/>署名の中身は検証せずに通す"]
  F --> G["通常の管理画面にアクセスできる"]

二要素認証の画面には、ブラウザ側で使うチャレンジと、登録済み認証器IDの一覧(allowCredentials)がそのまま埋め込まれている。
攻撃者はチャレンジを取得し、ホスト名やフラグを合わせた応答を作って webauthn_request_response として送る。
PoCの検証環境では、データベースに直接書き込んで認証器ID(0xA1を32バイト並べた値)を登録しておき、そのIDを固定値で使っている。
署名欄には not-a-real-signature-PMA-WEBAUTHN-BYPASS という文字列をBase64にした値を入れて、二要素認証を通過できることを確認している。

ログインできても、操作可能な範囲はそのMySQLユーザーの権限内に限られる。
READMEも、サーバーのOSでコマンドを実行できるわけではないと明記している。
二要素認証はパスワード漏洩時の備えなので、署名検証を素通りできれば二要素認証として機能しなくなる。

公式配布物の同梱ファイルを確認する

PoCのREADMEと、phpMyAdminの開発元へ送った報告文(リポジトリ内の VENDOR-EMAIL.md)は、対象を「既定のtarアーカイブ(tarball)」と書いている。
理由として、composer install --no-dev では web-auth/webauthn-lib が入らないことを挙げている。
PoCの検証用Dockerfileのコメントにも「公式tarballには同梱されていない」と書かれている。

公式サイトから phpMyAdmin-5.2.3-all-languages.tar.gz をダウンロードして中身を確認すると、vendor/web-auth/webauthn-lib/ があり、Webauthn\Server クラスを定義する src/Server.php も入っていた。
同梱の vendor/composer/installed.json によると、バージョンは web-auth/webauthn-lib v3.3.12。
phpMyAdmin-5.2.3-english.zip にも同じファイルがあった。

公式リリース用のスクリプト scripts/create-release.sh を確認すると、このスクリプトは composer update --no-dev のあと、推奨扱いのパッケージを composer require で追加している。
追加する一覧には web-auth/webauthn-lib が含まれる。
さらに最後の確認で vendor/web-auth/webauthn-lib/src/Server.php がなければ、「Webauthn-lib should be installed」と表示してリリース作業を止める。

公式配布物では class_exists(WebauthnServer::class) が真になるので、WebauthnLibServer が選択される。
PoCのREADME自身も、ライブラリを入れた環境は WebauthnLibServer が使われ、この問題の対象外だと書いている。
PoCの検証用Dockerfileは公式tarballを使わず、利用者が src/ に置いた5.2.3のソースに composer install --no-dev を実行し、そのあと vendor/web-auth を削除してから動かす作りになっている。
5.2.3のタグには composer.lock が含まれており、そこでは webauthn-lib が通常依存として記録されているためだ。
リリーススクリプトは推奨パッケージを追加した状態の composer.lock をリリース用のコミットにだけ入れ、タグを打ったあとブランチからは削除している。
composer install --no-dev はロックファイルの通常の依存を入れるので、5.2.3のタグからこの手順で入れた場合もライブラリは入る。
Dockerfileのコメントでも「Gitのロックファイルにはwebauthn-libが残っている」と説明されている。

このように、同梱ファイルとコードの確認時点では、公式tarballはPoCの対象外と見られた。そこで、実際のDocker検証環境で両方の構成を動かして検証した。

Docker環境で両方の構成を動かして検証した

検証はポートを 127.0.0.1 のループバックにのみバインドしたDockerコンテナで行った。
MySQLとphpMyAdminはどちらもコンテナ内で、ログインに使う認証情報(appuser / apppass)と登録済み認証器のレコードは、初期化SQLで投入した。
外部のサーバーにはアクセスしていない。

検証した構成は次の3通り。
どれが CustomServer でどれが WebauthnLibServer かは、分岐条件の class_exists('Webauthn\Server') の値を、それぞれのコンテナ内で php -r を実行して確かめた。

構成入手元依存関係の処理使われる実装
AGitタグ RELEASE_5_2_3composer install --no-dev 後に vendor/web-auth 削除(PoCの手順)CustomServer
B公式tarball phpMyAdmin-5.2.3-all-languages.tar.gzそのまま展開(vendor は無改変)WebauthnLibServer
C公式tarball(TLS追加)自己署名証明書によるHTTPS化と bcmath・gmp 拡張を追加WebauthnLibServer

構成Aでの実行

PoCのリポジトリを git clone し、lab/src/ に5.2.3のタグを置いて ./run.sh を実行した。
出力はREADMEの記載どおりだった。

IOC password-login status=200 webauthn=True
IOC challenge=PtcxeEGeDNgeIebwFs4/6aXY…
IOC 2fa-post status=200 still_2fa=False landed=True
SUCCESS PMA-WEBAUTHN-BYPASS WebAuthn 2FA accepted with garbage signature

still_2fa=False は二要素認証の画面が消えたこと、landed=True は通常のphpMyAdmin画面の目印が見つかったことを示す。
署名欄には not-a-real-signature-PMA-WEBAUTHN-BYPASS という文字列だけを入れ、秘密鍵は使っていない。

構成Bでの実行

同じPoCを公式tarballの構成に向けると、二要素認証は通らなかった。
ただし失敗した原因は署名ではなく、HTTP接続によるものだった。

IOC 2fa-post status=200 still_2fa=True landed=True
Two-factor authentication failed: Invalid scheme. HTTPS required.

webauthn-lib v3.3.12 の AuthenticatorAssertionResponseValidator は、署名を検証する2つ前の段階で、clientDataJSON のオリジンが https かどうかを見ている。
例外扱いになるのは「安全なRelying Party ID(認証要求元のRP ID)」の一覧に登録されたホスト名だけで、その一覧(securedRelyingPartyId)の既定値は空だ。
phpMyAdminの WebauthnLibServer は、この一覧を設定する setSecuredRelyingPartyId() を一度も呼んでいない。
HTTPでアクセスしている限り、localhost でも 127.0.0.1 でもこの検査で弾かれる。

これでは署名検証で拒否されたのか、その手前で弾かれたのかを区別できなかった。
そこでTLSを付けた構成Cを作った。

構成Cで署名だけを3通りに変えて比較する

構成Cでは、本物のセキュリティキーの代わりに使うP-256(ECDSA)鍵ペアを作成し、公開鍵をCOSE形式(WebAuthnが鍵を表現するのに使う形式)にして初期化SQLで登録した。
秘密鍵はホスト側に保持し、検証スクリプトから署名させた。
アクセス先はHTTPS(https://localhost:18443)に変えた。

各モードは clientDataJSON と authenticatorData の生成手順を揃え、signature の値のみを変えている。

モードsignature の中身結果phpMyAdminの表示
garbage41バイトの文字列。PoCと同じ not-a-real-signature-PMA-WEBAUTHN-BYPASS拒否Invalid data. Should start with a sequence.
wrongkey別のP-256鍵で作った、形式は正しい64バイトのES256(ECDSA P-256)署名拒否Invalid signature.
valid登録した鍵自身で作った正しいES256署名通過-
IOC pma-says: Two-factor authentication failed: Invalid data. Should start with a sequence.
CONTROL-GARBAGE-REJECTED forged signature rejected
IOC pma-says: Two-factor authentication failed: Invalid signature.
CONTROL-WRONGKEY-REJECTED forged signature rejected
IOC 2fa-post status=200 still_2fa=False landed=True
CONTROL-VALID-ACCEPTED genuine signature accepted => harness is correct

valid(正規署名)が通ることで、チャレンジ・オリジン・rpIdHash・認証器ID・鍵の登録がすべて正しく設定できていることが確認できた。
そのうえで wrongkey だけが Invalid signature. で弾かれるため、この拒否は署名検証そのものによるものと分かる。
Invalid signature. は AuthenticatorAssertionResponseValidator の検証手順7.2.16、登録済みの公開鍵で $algorithm->verify() を呼ぶ行の失敗メッセージだ。

garbage だけ表示が異なる理由は、署名検証の直前に署名を正規化する CoseSignatureFixer::fix() が呼ばれるためだ。
ES256の署名は64バイトならそのまま使うが、それ以外はASN.1のDER符号化(データ構造のバイナリ表現形式)として読み直す。
41バイトの文字列はDERとして解釈できないので「Should start with a sequence.」で止まり、verify() は呼ばれない。
PoCはこの値を用いているため、公式tarballに対してPoCをそのまま実行しても、署名検証の処理まで進まない。

検証時に引っかかった箇所

構成Bを最初に動かしたときは、二要素認証の画面が出る前にエラーになった。

Internal error: Assert\InvalidArgumentException: The parameter "aaguid" is missing

webauthn-lib の PublicKeyCredentialSource::createFromArray() は、保存された認証器のレコードにクラスの全プロパティがそろっている必要がある。
PoCの初期化SQLが作るレコードには aaguid(認証器の機種を識別する16バイト)が入っていない。
CustomServer はログイン時に aaguid をチェックしないので、これで通ってしまう。
構成Cでは aaguid に36文字のUUID文字列を入れて解消した。

2つ目は拡張機能で、Two-factor authentication failed: Requires GMP or bcmath extension. が発生した。
メッセージの出所は webauthn-lib が依存する fgrosse/phpasn1 で、楕円曲線の計算にGMPかbcmathのどちらかを要求する。
php:8.2-apache の既定ではどちらも入っていなくて、phpMyAdminの composer.json の推奨(suggest)にも載っていない。
構成Cでは両方を入れた。

3つ目は署名回数のカウンターで、valid が成功すると、ライブラリは登録済みレコードのカウンターを応答の値で上書きして保存する。
カウンターの検査は「新しい値が保存済みの値より大きいこと」なので、同じ値でもう一度 valid を実行すると失敗する。
検証スクリプトでは valid を最後に実行し、やり直すときはデータベースごと作り直した。
署名の検証はカウンターの検査より前にあるので、garbage と wrongkey の結果はカウンターの状態に影響されない。

影響するインストール方法

インストール方法webauthn-lib使われる実装確認
公式サイトの5.2.3のtarball・zip同梱(v3.3.12)WebauthnLibServer(対象外)実測(構成B・C)
Docker公式イメージ(5.2.3)公式tarballを展開するので同梱WebauthnLibServer(対象外)tarballのみ実測
Gitの5.2.3タグ(RELEASE_5_2_3)から composer install --no-devcomposer.lock にあるので入る(v3.3.12)WebauthnLibServer(対象外)実測
Gitの開発ブランチ(QA_5_2)から composer update → --no-dev入らないCustomServer(対象)実測
Gitの開発ブランチ(master)から composer update → --no-dev入らないCustomServer(対象)実測
Debianのphpmyadminパッケージ依存関係にないCustomServer になると考えられる未検証

Gitから入れる方法は、5.2.3同梱のドキュメント(doc/html/_sources/setup.rst.txt)に載っている。
リポジトリを git clone し、composer update のあと、「開発しないなら開発用ツールを省ける」として composer update --no-dev が案内されている。

表のGitに関する3行は、2026年10月29日に各ref(ブランチ・タグ)を php:8.2-cli のコンテナでクローンし、Composerを実際に走らせた結果。
QA_5_2(コミット 6b04ffd)には composer.lock がなく、master(コミット 6016908)の composer.lock ではwebauthn-lib 5.3.9が開発用の側に入っている。
どちらも composer update では vendor/web-auth が入り、続く composer update --no-dev で消えた。
タグ RELEASE_5_2_3 は逆で、composer install --no-dev を実行すると vendor/web-auth/webauthn-lib/src/Server.php が入り、class_exists が真になった。

注意点として、PHPのzip拡張が入っていないと、開発用依存の php-webdriver/webdriver が ext-zip を要求して、依存の解決そのものが失敗する。
--no-dev を付けても require-dev の解決は行われるためで、Composerも「--no-dev は require-dev を無視する意味ではない」と表示する。
zip拡張を入れて再実行すると、上記の結果になった。

なお、ブランチのコードは開発中の最新版であり、リリース済みの5.2.3とは異なる。

Debianは、trixie(5.2.2)とsidのどちらのパッケージも、依存・推奨・提案のいずれにもWebAuthnのライブラリを挙げていない。
依存関係を調べると、DebianのパッケージはPHPのライブラリをDebianの別パッケージから読み込む作りで、web-auth の名前を持つパッケージはDebianのソース検索で見つからなかった。
この条件なら Webauthn\Server は読み込めないはずだが、実機で確かめてはいない。

どのインストール方法であっても、実際に影響を受けるのは「CustomServer が使われる環境」「攻撃者がMySQLのユーザー名とパスワードを保持」「そのユーザーが二要素認証にWebAuthn(Hardware Security Key)を登録」の3条件をすべて満たす場合に限られる。
認証アプリのワンタイムコードを使っているユーザーは、この処理を通らない。

開発元の状況

2026年10月29日時点で、phpMyAdminのセキュリティ告知の一覧の最新は2025年1月のPMASA-2025-3で、この件の告知はない。
GitHubリポジトリにも公開済みのセキュリティアドバイザリ(脆弱性勧告)はなく、CVE番号も見つからなかった。
PoCリポジトリの VENDOR-EMAIL.md は security@phpmyadmin.net 宛ての報告文の形をとっているが、実際に送った日付や返信の有無は書かれていない。

5.2系の開発ブランチ QA_5_2 には、2026年9月29日に「Fix WebAuthn\CustomServer credential save」というプルリクエスト(#20499)がマージされている(コミット)。
中身は、ログイン時に保存済みの認証器情報を探して、署名回数のカウンターを保存し直す変更だ。
登録済みの情報が見つからなければ失敗するようになったが、公開鍵で署名を検証する処理は入っていない。
QA_5_2(コミット 6b04ffd)の libraries/classes/WebAuthn/CustomServer.php と、master(コミット 6016908)で src/WebAuthn/CustomServer.php に移った同じクラスを確かめても、署名を検証するコードは見つからなかった。
このプルリクエストが今回の報告と関係するかどうかはコミット内容からは判断できない。

確かめ方と当面の対処

実機でどちらの実装が動くかは、Webauthn\Server クラスを読み込めるかで決まる。
Composerの標準配置なら、インストール先で次のファイルが存在するかで判別できる。

ls vendor/web-auth/webauthn-lib/src/Server.php

ファイルが存在すればライブラリ版の WebauthnLibServer が使われるため今回の問題は当てはまらず、なければ CustomServer が動く。
ライブラリを別の場所から読み込む構成もあるので、確実に知りたい場合はphpMyAdminと同じ読み込み設定のもとで class_exists('Webauthn\Server') が真になるかで確かめられる。

CustomServer が使われる環境での対処は、PoCのREADMEが挙げる案と同じになる。
web-auth/webauthn-lib を入れてライブラリ版に切り替えるか、修正が出るまでWebAuthnの二要素認証に頼るのをやめて、phpMyAdmin自体をインターネットから直接アクセスできないネットワークに配置する。
Gitから入れた環境なら、使っているブランチの composer.json の require-dev に書かれた版(QA_5_2 は ^3.3.2、master は ^5.3)を composer require で追加すればよい。
Debianのパッケージを使っている場合はパッケージ管理の外でライブラリを足すことになるので、IPアドレスでの制限やVPN経由に限定するなど、アクセス元を制限する対処が現実的だ。

ライブラリ版に切り替えるときの前提

ライブラリ版に切り替える際は、今回の検証で判明した2つの注意点がある。

1つ目はPHP拡張機能で、前述のとおりGMPかbcmathのいずれかがないと、署名検証の前に Requires GMP or bcmath extension. で失敗する。
phpMyAdminの composer.json の推奨には載っていないので、composer install の出力には表示されない。

2つ目は既存の認証器レコードで、CustomServer が保存した16バイトの生バイナリ形式の aaguid は、webauthn-lib では解釈できずエラーになる。
コンテナ内でPHPから直接この関数を呼んで確かめたところ、16バイトがすべて 0x00 のレコードは Safe\Exceptions\UrlException で失敗した。
0x80 以上のバイトを含む16バイトでは、その手前の保存の段階で失敗する。
phpMyAdminが設定の保存に使う json_encode() が不正なUTF-8を理由に false を返し、UserPreferences::save() はその戻り値を検査せずにSQLへ渡しているためだ。

この2点はPHPの関数を直接呼び出して確認した挙動。もしライブラリ導入後に二要素認証でエラーが出る場合は、認証器を一度削除して再登録することで解消できる。