
CVE-2026-85706(GitLab CE/EEにおけるWorkhorseのパスエンコーディングバイパスを悪用した未認証の任意ファイル読み取り)のPython PoCエクスプロイト。解説記事とバイパス手法のバリエーション付き。
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 によるもので、このリポジトリは私の writeup と 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 の署名ラウンドトリッパーはプロキシするすべてのリクエストに有効な JWT を付与するため、汎用 API プロキシにフォールスルーするものはすべてこのチェックを通過する。
Gitlab-Workhorse-Api-Requestこれが誰にとっても即座の 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 の content type が重要なのである。
修正(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 になる)。