Open2

CVE勉強ノート

ちゃちゃちゃちゃ

CVE-2025-8264

https://security.snyk.io/vuln/SNYK-PHP-ZPUSHZPUSHDEV-10908180

概要

  • 対象ソフトウェア : z-push/z-push-dev

    • ActiveSync互換デバイスを同期するためのオープンソースアプリケーション
      • ActiveSync : MicrosoftのPCがモバイルと同期するためのプロトコル(Exchangeのメールサーバーでの使用が有名)
  • 脆弱性 : SQLインジェクション

  • 対象のソフトウェアの中でIMAPバックエンドを利用し、IMAP_FROM_SQL_QUERYを設定として使っている部分で発生

脆弱なポイント

$sql = str_replace('#username', $username, str_replace('#domain', $domain, IMAP_FROM_SQL_QUERY));

そもそもこのコードでは何をしたかったの?

  • 検索用のSQLのクエリを組み立てたい
    IMAP_FROM_SQL_QUERYは、以下のようになっていて、#username#domainをユーザー入力の$username$domainに置き換えて、メールアドレスを作って、usersテーブルからユーザーを検索するためのクエリを作りたかった。
define('IMAP_FROM_SQL_QUERY', "select first_name, last_name, mail_address from users where mail_address = '#username@#domain'");

なんで脆弱?

  • パラメーター化されておらず、SQL構造と値の分離が行われていなかったため、SQLインジェクションに対して脆弱

修正後

$sql = str_replace("'#username'", ":username", str_replace("'#domain'", ":domain", str_replace("'#username@#domain'", ":usernameatdomain", IMAP_FROM_SQL_QUERY)));

$usernameatdomain = $username . '@' . $domain;

if(mb_strpos($sql, ':username') !== false) $sth->bindValue(':username', $username, PDO::PARAM_STR);
if(mb_strpos($sql, ':domain') !== false) $sth->bindValue(':domain', $domain, PDO::PARAM_STR);
if(mb_strpos($sql, ':usernameatdomain') !== false) $sth->bindValue(':usernameatdomain', $usernameatdomain, PDO::PARAM_STR);

何をどういう風に修正した?

  • PDOのプレースホルダ(:変数名)を使用することで、パラメーター化が行える

なんで安全?

  • '#username' 等を :username などの PDO プレースホルダに変換し、PDOの関数を適切に使用することによって「値」として渡すように変更
    • SQL の構造(SELECT/WHERE 等)と値が完全に分離され、値に含まれる ' OR 1=1 -- のような文字列は ただの値として処理され、SQL構造を変えることはできないため
ちゃちゃちゃちゃ

早速昨日サボったので、今日は二つやる

CVE-2025-57756

https://security.snyk.io/vuln/SNYK-PHP-CONTAOCOREBUNDLE-12239890

概要

  • 対象ソフトウェア : contao/core-bundle
    • 保守が容易でプロフェッショナルなウェブサイトを求める人のためのオープンソースの PHP 製CMS
  • 脆弱性 : フラグメントのレンダリング処理における認可不備
  • 発生ポイント : 検索機能

脆弱なポイント

モジュールの描画部

// Disable indexing if protected
if ($objModule->protected && !preg_match('/^\s*<!-- indexer::stop/', $strBuffer)) {
    $groups = StringUtil::deserialize($objModule->groups, true);

    if (\count($groups) !== 1 || !\in_array(-1, array_map(\intval(...), $groups), true)) {
        <SNIP>
    }
}

// コンテンツの描画部
// Disable indexing if protected
if ($objElement->protected && !preg_match('/^\s*<!-- indexer::stop/', $strBuffer)) {
    $groups = StringUtil::deserialize($objElement->groups, true);

    if (\count($groups) !== 1 || !\in_array(-1, array_map(\intval(...), $groups), true)) {
        <SNIP>
    }
}

そもそもこのコードでは何をしたかったの?

  • 保護されたモジュール/要素なら、検索のインデックス対象から外す(=indexer::stop を付ける)処理

なんで脆弱?

  • フラグメントとして描画するとき、実際に表示しているレコードを表す変数ではなく、$objModule / $objElement を見ていたため、ケースによって保護判定がずれ、保護済みコンテンツがインデックスされてしまうことがあった
    • フラグメントとして描画
      • 断片ごとに描いて組み立てる方式

修正後

// Disable indexing if protected
if ($objRow->protected && !preg_match('/^\s*<!-- indexer::stop/', $strBuffer)) {
    $groups = StringUtil::deserialize($objRow->groups, true);

    if (\count($groups) !== 1 || !\in_array(-1, array_map(\intval(...), $groups), true)) {
        <SNIP>
    }
}

何をどういう風に修正した?

  • 判定対象を $objModule$objElementから$objRow に統一

    • つまり、実際にレンダリングしている1件に対して常に保護判定を行うようにした

    なんで安全?

  • 参照の齟齬がなくなる

    • 保護付きフラグメントも例外なくインデックスから除外される
  • 既存の除外条件(indexer::stop コメントやグループ判定)のロジック自体は維持

    • 意図した公開/非公開の境界を正しく反映できるようになった

要は、本来は公開しないコンテンツがフラグメントとして検索インデックス化しているとき、公開しないか公開するかの条件分岐がずれることによって、公開したくないページも検索結果に表示されるってこと