
CVE-2026-103978 の概念実証および解説記事。OPNMGR の snyk_scan_progress.php における認証不要のパストラバーサルであり、任意の .json ファイルの読み取りが可能。
任意の .json ファイルの読み取りが、ログイン不要で可能です。PHP で書かれた OPNsense フリートマネージャーである agit8or1/OPNMGR のソースを読んでいて発見しました。
| CVE | CVE-2026-103978 |
| Advisory | GHSA-8x7v-vwpx-3wr7 |
| Class | CWE-22 (Path Traversal) |
| Auth | none |
| CVSS v3.1 | 7.5 High (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| CVSS v4.0 | 8.7 High |
| Fixed in | v3.10.0 |
| Credit | Kanarat Kaeothong (Axiom0x) |
snyk_scan_progress.php はウェブルートに置かれており、クエリ文字列から scan_id をそのまま受け取り、ファイルパスに貼り付けて、その結果を file_get_contents() に渡します。認証の include も、セッションチェックも、パラメータのサニタイズもありません。
<?php
header('Content-Type: application/json');
$scan_id = $_GET['scan_id'] ?? ''; // unauthenticated, raw
if (empty($scan_id)) { echo json_encode(['error' => 'No scan ID provided']); exit; }
$progress_file = "/tmp/snyk_scan_{$scan_id}.json"; // string concat, nothing else
if (!file_exists($progress_file)) { echo json_encode(['error' => 'Scan not found']); exit; }
$progress_data = file_get_contents($progress_file);
echo $progress_data; // sent back to the caller
アプリ内の他のすべてのエンドポイントは、何かを行う前に inc/bootstrap.php を読み込み、requireLogin() を呼び出します。このエンドポイントだけはそれをすべて省略しています。そのため scan_id は攻撃者が制御でき、そのままパスに挿入され、ファイルの内容がレスポンスとして返されます。
1つ注意点があります。プレフィックス snyk_scan_ は送信した内容にそのまま結合されるため、.. で始まるペイロードは飲み込まれます(snyk_scan_.. は /tmp 内の単なる奇妙なファイル名にすぎません)。.. を実際に親ディレクトリへの移動として機能させるには、まず捨てセグメントが必要です。
/tmp/snyk_scan_ + x/../../etc/passwd -> /tmp/snyk_scan_x/../../etc/passwd
-> /etc/passwd.json
末尾の .json はコードによって強制され、現代の PHP にはヌルバイトトリックもないため、読み取れるのは .json で終わるファイルだけです。実際の PHP デプロイメントでは、これはほとんど制限にはなりません。設定ファイル、サービスアカウントキー、composer の auth.json、クラウド認証情報など、おいしいもののほとんどはすでに .json です。
GET /snyk_scan_progress.php?scan_id=x/../../var/www/opnsense/config
-> returns /var/www/opnsense/config.json
GET /snyk_scan_progress.php?scan_id=x/../../../../etc/some_config
-> walks to filesystem root, returns /etc/some_config.json
実行可能なバージョンは poc/poc.sh にあります。
www-data が読み取れる .json で終わる任意のファイルが、このページに到達できる誰にでも開示されます。OPNsense ファイアウォールを管理するマシンでは、通常それは API キーと認証情報を意味します。
アプリの他の部分が使用している認証 include を追加し、パスを構築する前に scan_id を厳格な許可リストで検証します。
require_once __DIR__ . '/inc/bootstrap.php';
requireLogin();
$scan_id = $_GET['scan_id'] ?? '';
if (!preg_match('/^[A-Za-z0-9_-]{1,64}$/', $scan_id)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid scan id']);
exit;
}
$progress_file = '/tmp/snyk_scan_' . $scan_id . '.json';
メンテナーは v3.10.0 で修正をリリースしました。
協調的開示を通じて報告されました。脆弱なバージョンはもはや最新リリースではありません。この記事は参考と学習のためにここにあります。