
CVE-2026-85706 の Python PoC と解説記事です。これは GitLab CE/EE における未認証の任意ファイル読み取りで、リポジトリエンドポイント上の Workhorse パスエンコーディングバイパスを悪用します。
GitLab CE/EE における未認証の任意ローカルファイル読み取り。18.7–19.1.7、19.2.0–19.2.5、19.3.0–19.3.1 に影響。19.1.8 / 19.2.6 / 19.3.2 で修正済み(2026-09-10)。CVSS 10.0、実環境での悪用が報告されている。元の報告は s3ntago によるもので、このリポジトリは単なる私の解説+PoC である。
この PoC は教育および防御的研究目的のみで公開されており、管理者や研究者が脆弱性を理解しテストするためのものである。所有しているシステム、または明示的な書面によるテスト許可を得たシステムに対してのみ実行すること。あなたのインスタンスが影響範囲に該当する場合は、読むのをやめてまず 19.1.8 / 19.2.6 / 19.3.2 にパッチを適用すること。
3 つのリポジトリエンドポイント(POST :id/repository/commits、POST/PUT :id/repository/files/:file_path)は Workhorse の requestBodyUploader の背後にある。Rails のハンドラは生の file.path リクエストフィールドからディスク上のパスを直接読み取り、認証より前に File.open する。require_gitlab_workhorse! は認証として機能しない。なぜなら Workhorse の署名付きラウンドトリッパーは、プロキシするすべてのリクエストに有効な Gitlab-Workhorse-Api-Request JWT を付与するため、汎用 API プロキシにフォールスルーするものはすべてこのチェックを通過してしまうからだ。
これが誰にとっても即座の LFI にならない唯一の理由は、Workhorse が本来リクエストを先に書き換えることになっているからである。しかしそのルート正規表現はエスケープされたパス(EscapedPath() に加え、%XX を決してデコードしない path.Clean クローン)に対してマッチする一方、Puma は Grape ルーティングの前に %XX をデコードする。したがって、静的セグメントの任意の文字をパーセントエンコードする(%63ommits、%72epository、%66iles)か、末尾にスラッシュを付けるか、Workhorse の正規表現が取りこぼす .json を付ければ、Rails は依然として脆弱なハンドラにルーティングする。このエンコーディングの不一致こそがバイパスの所在である。(//、/./、%2F、; といったこの種のバリアントは機能しない。path.Clean が最初の 2 つを正規化し、Puma が %2F を拒否するからである。)
あとは、偽造した未署名のアップロードメタデータをクエリパラメータとして送信するだけである:
POST /api/v4/projects/1/repository/%63ommits?file=&file.path=<ABSOLUTE_PATH>&file.size=1&Content-Type=application/x-www-form-urlencoded
file= を空にすると requires :file, WorkhorseFile のバリデーションを満たします(空値は nil に変換されます)。読み取りは認証前に発火します。バイト列を取り戻すのが面白いところです。urlencoded ブランチでは、ヘルパーが Rack::Utils.parse_nested_query(File.read(path)) を実行し、パーサーのエラーを 400 のレスポンスボディに補間します。2 桁の 16 進数が続かない % は InvalidParameterError: invalid %-encoding (<raw file bytes>) を発生させ、ファイルの内容がエラーメッセージ内に返ってきます。JSON ブランチ(Oj)は何も漏らさないため、ここでは urlencoded のコンテンツタイプが重要になります。
修正(master 0d9ce3e7、バックポート 1fe30154 / b43c8b26 / 0ff7b6b2)は、3 つのエンドポイントすべてに authenticate! を追加し、さらに /authorize の事前ステップも加え、パス/サイズについてはミドルウェアが生成した UploadedFile のみを信頼し、パーサーのエラーをエコーバックするのをやめています。このバグは 2025 年 12 月に導入されたため、影響範囲が 18.7 から始まっています。
./exploit.py --url http://localhost:8080 --file /etc/hostname
./exploit.py --url http://localhost:8080 --file /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
スクリプトは、検証済みのすべてのバイパス形式(エンコードされたセグメント、末尾スラッシュ、.json、commits + files エンドポイント、POST と PUT)を順に試し、各レスポンスを分類するので、チェーンのどこでプローブが失敗したかを特定できます。当然ながら、許可されたターゲットのみに使用してください。
% が含まれている場合のみです。) まで (.*) でマッチ)。標準の JSON エラーに対しては問題ありませんが、手前の何かがレスポンスを HTML でラップしている場合は過剰にキャプチャします。適切な修正は JSON の message フィールドをパースすることです。before ブロックが 404 になります)。