Security Blog
Posted By Gregory

Under The Hood 9.8.3


English version: Under The Hood 9.8.3


今回のリリースは、新機能のリリースというよりは、メンテナンスとセキュリティ強化のためのものです。作業の大部分は、サイト所有者が普段目にすることのない部分、つまりセキュリティログを読み取るデータレイヤー、通知メールを作成するコード、アクティビティレコードのCSVファイルを出力するエクスポートパスで行われました。これらの変更によってプラグインの外観は変わりませんが、サイトが混雑しているとき、ログが大きいとき、あるいは攻撃者がまだ解決できていないエッジケースを突いたときなど、プラグインの動作の信頼性と安全性が向上します。

以下は、実際に何が変わったのかを理解したい管理者や開発者向けの、実装レベルでの説明です。

メール受信者へのインジェクション攻撃を阻止する

WP Cerber は、個人の名前を埋め込んだ 2 種類のトランザクション メールを送信します。それは、2 要素認証 PIN メッセージとアクティビティ アラート通知です。どちらも、受信者を一般的な RFC 5322 形式であるName <email>で構築し、その名前をエスケープせずに WordPress プロファイル フィールド ( user_firstnameuser_lastnamedisplay_name ) から直接取得します。

問題は、 wp_mail()文字列の受信者を処理する方法にあります。wp_mail() は文字列をリテラルのカンマで分割し、引用符で囲まれた文字列の境界を尊重しません。そのため、適切な文字を自分のプロファイル名に挿入したユーザーはCRB_2FA::send_user_pin()によって生成される 2FA PIN メールと、 cerber_get_email()user_listブランチによって生成されるアラート メールの受信者リストに、余分なアドレスを挿入できてしまいます。誰でも登録して表示名を設定できるサイトでは、これはセキュリティメッセージのコピーを密かにリダイレクトする現実的な手段となります。

cerber-common.phpに専用のサニタイザー関数crb_sanitize_mail_display_name()を追加し、呼び出し元両方に適用しました。この関数は、この種のインジェクション攻撃で問題となる文字、つまりカンマ、引用符、バックスラッシュ、山括弧、および制御文字を正確に削除します。正規のユーザーには引き続き正常に名前が表示され、細工されたプロファイルフィールドによって受信者リストを操作することはできなくなります。

所有権スキャナに保存されているXSSベクターを削除する

マルウェアスキャナーは、WordPress.orgリポジトリでインストール済みのプラグインの所有権が変更された際にそれを報告します。そのためには、リポジトリから提供される所有権メタデータを利用し、これまでは、そのメタデータの一部を、所有者プロファイルへのリンク用のアンカータグを手動で作成するなどして、生のHTMLとして管理画面の通知に組み込んでいました。

そのリポジトリは信頼できる情報源であり、悪意のあるメタデータが存在する可能性は低いものの、外部データがエスケープされていないマークアップとして管理インターフェースに到達することは決してあってはなりません。それを信頼できる情報源として扱うことは、まさに私たちが依存したくない前提なのです。

ベクトルをローカルでパッチするのではなく適切に閉じるために、ヘルパー関数crb_ui_formatted_text()を介して構築される新しい UI ファクトリ要素formatted_textを導入しました。これは、番号付きの%N$sプレースホルダーを含むプレーンテキストテンプレートをレンダリングします。テンプレートのリテラル部分とスカラー引数は HTML エスケープされ、UI 要素である引数はアクティブなレンダラーを介してレンダリングされます。したがって、各動的値は実際に出力されるコンテキストでエスケープされるため、出力コンテキストの誤りを確実に防ぐことができます。

crb_check_ownership()の所有権メッセージではこの要素が使用されるようになり、所有者プロファイルリンクは連結されたアンカー文字列ではなくcrb_ui_link()を使用して構築されるため、プロファイル URL と表示名はそれぞれ URL とテキストとしてエスケープされます。翻訳文字列は変更されていないため、ローカライズは引き続き機能します。保存された管理者 XSS のリスクは解消されました。

トラフィックインスペクターの「ソフトウェアエラー」フィルターを修正する

トラフィックインスペクターログの高度な検索フォームでは、複数の条件を組み合わせることができます。その一つが「ソフトウェアエラー」チェックボックスです。このチェックボックスを他のフィルターと組み合わせると、PHPエラーが記録されたリクエストが、指定した他の条件に一致しない場合でも、検索結果に表示されることがありました。

根本原因は、WHERE句の組み立て方法における演算子の優先順位にありました。従来のコードでは、フィルタの断片が文字列として連結され、グループ化されたエラー条件が括弧で囲まれていなかったため、その中のOR演算子が周囲のAND演算子を回避できていました。トラフィッククエリをクエリビルダーに移行した結果、グループ化されたエラー条件が正しく括弧で囲まれるようになり、結合されたフィルタはフォームが示すとおりに動作するようになりました。検索を絞り込むと、設定したすべての条件が結果に反映されるようになりました。

一致する検索ワイルドカード

以前の高度なトラフィックログ検索では、検索語に%または_を入力すると、エスケープ処理されずにパターン内に配置されるため、 LIKEメタ文字として機能していました。これは、せいぜい意図しない機能であり、最悪の場合は不要な読み込みパターンです。

すべてのLIKE用語は、周囲のワイルドカードが追加される前にCRB_Database::escape_like()を通過するようになったため、 %_ユーザーが入力した文字としてそのままマッチします。検索は予測可能な動作をし、クエリが意図した範囲を超えてスキャンするように変更されることはありません。

エクスポート時のメモリ制限処理の修正

大量のアクティビティログやトラフィックログをエクスポートするとメモリを大量に消費するため、WP Cerber は処理前に使用可能なメモリを増やします。しかし、一部の環境では、 512などの数値メモリ制限値がメガバイトではなくバイトとして解釈されていました。その場合、プラグインは意図したとおりに制限値を引き上げることができず、エクスポートが予想よりも早く停止してしまうことがありました。

値が正しい単位で解釈されるようになったため、メモリ増加は設計どおりに適用され、以前影響を受けていた環境でも大規模なエクスポート処理が完了するようになりました。

ストリーミングに関するログエクスポートの再構築

アクティビティとトラフィックのエクスポートでは、一致する行をチャンク単位で読み込み、各チャンクごとにOFFSET増やしながら同じSELECT再実行します。大きなログでは、ディープオフセットスキャンに劣化し、各チャンクのコストが前のチャンクよりも高くなり、メモリに結果がどんどん蓄積されます。

どちらのエクスポートも、 CRB_Database::query_stream()をバッファリングせずに一度だけ通過させ、一度に 1 行ずつ出力するジェネレーターでラップして行を読み取ります。一致するレコードの数に関係なくメモリ使用量は一定に保たれ、データベースはチャンクごとに 1 回ではなく 1 回だけ処理を実行します。バッファリングされていないストリームは処理中に接続をロックするため、行の合計と日付範囲は、ストリームを開く前に、同じフィルターから構築された個別のバッファリングされたCOUNTMIN / MAXクエリを使用して事前に解決されます。

共有ファイルダウンロードヘルパーcrb_file_headers()に 2 つのレスポンス ヘッダーが追加されました。X X-Accel-Buffering: no 、Nginx が PHP-FPM のフロントエンドである場合、エクスポート全体をバッファリングしてから送信するのではなく、各チャンクをすぐに転送するように指示します。これにより、最初のバイトまでの時間が改善され、プロキシが大きな CSV をメモリに保持することが回避されます。このヘッダーは Nginx 固有のものであり、mod_php を使用する Apache や他のプロキシでは無害に無視されます。Cache Cache-Control: no-store 、ブラウザまたは中間プロキシが機密性の高いセキュリティ ログのエクスポートをキャッシュすることを防止します。

また、ストリームのクリーンアップも決定論的にしました。エクスポート処理では、 try / finallyブロック内でジェネレーターを消費し、 finallyブロック内で唯一残っているハンドルを解放します。そのため、バッファリングされていない結果は、正常終了、早期停止、行の書き込み中に例外が発生した場合など、すべての終了パスで解放され、接続もロック解除されます。以前は、完全に消費される前に早期に中断したりエラーが発生したりすると、結果が開いたままになり、接続がロックされたままになる可能性があり、その結果、そのリクエスト内の次のクエリが失敗する原因となっていました。

エクスポートされた日付範囲を報告する

以前は、CSVエクスポートのヘッダーには有効なフィルタのみが反映されていました。アクティビティとトラフィックの両方のエクスポートで、エクスポートされたデータに含まれる最も古いレコードと最も新しいレコードのタイムスタンプを示す2行がヘッダーに追加されるようになりました。ヘッダーは最初の行より前に書き込まれるため、範囲はエクスポートと同じフィルタ付きクエリから構築されたMIN / MAXクエリによって取得され、一致する行がない場合は省略されます。エクスポートをアーカイブすると、ファイルにはそれが表す正確な時間範囲が記録されるようになりました。

輸出の失敗を隠蔽するのではなく、可視化する

従来のエクスポート処理では、エラーが発生しても何も表示されずに処理が停止してしまうことがありました。データベースが利用できない場合や、行ストリームを開くことができない場合、以前のコードは説明のない空のCSVファイルを生成する傾向があり、これはインシデント発生時にレコードを取得しようとするユーザーにとって最悪の結果でした。

両方のログの読み取りおよびエクスポートのパスが再設計され、データペイロードまたは構造化エラーactivity_export_query_build_failedactivity_export_db_unavailable 、a activity_export_stream_failedなどの明確なコードを含む)のいずれかを含むRevalt結果が返されるようになりました。下位レイヤーの障害が結果に連鎖されるため、元の根本原因は破棄されずに保持されます。セットアップの失敗は、空のファイルをストリーミングする代わりに、CSV バイトが 1 つも送信される前にwp_die()を使用してエクスポートを終了するようになりました。

エクスポートが失敗した場合、 manage_options権限を持つ管理者は、メッセージに付加された連鎖的な根本原因(例えば、基となるデータベースエラー)を確認できます。この権限を持たないユーザーには表示されないため、対応可能な詳細情報は対応できる担当者に届き、データベース内部の詳細は他のユーザーには表示されません。メッセージはcrb_escape_html()によってエスケープされます。

アクティビティログとトラフィックログをドメインクラスに統合する

このサイクルにおける大きな変更点は構造的なものでした。アクティビティログとトラフィックログの両方において、SQL文字列と結果処理がダッシュボードとエクスポートコード全体に散在していました。そこで、これらのロジックをCRB_ActivityクラスとCRB_Traffic_Logクラスに移動しました。これにより、クエリの構築と行の取得は、プレゼンテーションコードを生のSQLとして経由するのではなく、 fetch()stream_log()といった明確なメソッドの背後で実行されるようになりました。

両クラスとも、WHERE、JOIN、LIMIT句を手動で連結する代わりにwarp_get_db()で取得した DB Warp クエリビルダーを使用してクエリを構築するようになりました。これは単にコードの整理というだけでなく、重要な意味を持ちます。ユーザーが指定したすべてのフィルタ値を単一のエスケープ層にルーティングすることで、以前の手動エスケープ、 $wpdb->prepare() 、および手動で引用符を付けた連結が混在していた状態が解消されます。このような不整合は、インジェクションバグを隠蔽する原因となります。劣化したデータベース層がクエリを構築できない場合、コードはフィルタなしのクエリを実行するのではなく、一致しない条件で安全に失敗するようになりました。

これらのリファクタリングは内部的なものであり、画面表示には影響しませんが、上記のセキュリティと信頼性の修正を小規模で、局所的かつ検証可能なものにするための基盤となります。

アラートコードの抽出中に2つの潜在的なバグが発見されました

管理者アラートのディスパッチをCRB_Activity::log()から専用のCRB_Activity_Alertsクラスに移動したところ、数値インデックスのアライメントがずれていた位置配列が原因で発生した、以前から存在していた 2 つのバグが明らかになりました。

最初の問題は、アラートメール内のダッシュボードリンクに影響を与えました。疎なキーと密な値のペアリングによって値がずれてしまい、 filter_ipなどのリンクパラメータが、本来のアドレスではなくIPアドレス範囲の先頭を受け取ってしまうことがありました。値を名前付きキーにマッピングすることでこのずれが修正され、アラートメール内のリンクは正しい場所を指すようになりました。

影響を受けた検索文字列のユーザーマッチングに関する2つ目の問題。wp_get_current_user wp_get_current_user()というコードが、ユーザーIDが0の場合でもWP_Userオブジェクトを返していたため、マッチングに誤ったIDが使用されていました。現在はcrb_get_userdata()を使用してイベント自身のユーザーを検索し、ユーザーが見つからない場合にも対応しています。ユーザーに一致するアラートは、正しいユーザーに一致するようになりました。

より静かなダッシュボード:変更チェックにおけるオペレーター優先

CRB_Activity::is_modified_since() $stamp < $status['data_modified'] ?? PHP_INT_MAXを使用してタイムスタンプを比較していました。演算子の優先順位により、これは(... < ...) ?? PHP_INT_MAX PHP_INT_MAX とバインドされます。これにより、null を結合する部分がデッドコードとなり、 data_modifiedが存在しない場合は常に未定義キーの通知が発生していました。結合部分が括弧で囲まれたため、変更スタンプが欠落している場合は「変更済み」として扱われ、兄弟メソッドであるis_modified()と一致するようになり、誤った通知はなくなりました。

データベーススキーマ定義の一元化

最後の構造的な変更点はスキーマの保守です。インストールおよびアップグレードコードでは、ログテーブルにインラインのCREATE TABLE SQLを使用していたため、同じテーブルが複数の場所で記述される可能性がありました。これらの宣言は現在、単一のソースであるCRB_Schema_Definitionsから取得されるため、インストールおよびアップグレードは1つの正規定義に基づいて動作し、 CRB_Schema_Manager既存のインストールにおけるスキーマのずれをより一貫して検出できます。同様に、生のDROP INDEX SQLはCRB_Schema_Manager::drop_index_if_exists()に置き換えられました。

また、保存されたリクエストフィールドデータのデコード処理もより柔軟にしました。null許容の従来の値、空の値、無効なJSON、およびサポートされていないシリアル化されたペイロードはすべて、不正なレコードによって読み取りが中断されるのではなく、空の配列に解決することで、同じ安全な方法で処理されるようになりました。

このリリースが重要な理由

9.8.3には新しいボタンはありません。その代わりに、サイレントフェイルではなくセーフフェイルを実現するデータレイヤー、細工されたプロファイル名で操作できない通知メール、外部データを適切にエスケープする管理者通知、フラットメモリで実行され、問題が発生した際に通知するログエクスポート、そしてフォームに記述されたとおりに動作する検索フィルターが追加されました。これらは、セキュリティプラグインの長期的な信頼性を維持するために必要な変更であり、以前のコードが既に完璧であったかのように振る舞うよりも、こうした作業をオープンに行うことを優先しました。


I'm a team lead in Cerber Tech. I'm a software & database architect, WordPress - PHP - SQL - JavaScript developer. I started coding in 1993 on IBM System/370 (yeah, that was amazing days) and today software engineering at Cerber Tech is how I make my living. I've taught to have high standards for myself as well as using them in developing software solutions.

View Comments
There are currently no comments.