
根本原因分析、脆弱なDockerラボ、およびWorkhorse/Pumaパーサー差分を悪用したGitLabにおける未認証の任意ファイル読み取りであるCVE-2026-85706のPoCスクリプト。
CVSS 10.0 · 未認証 · 活発に悪用中 (CISA KEV)
GitLab Workhorse (Go リバースプロキシ) と Puma/Grape (Ruby) の間のパーサー差異により、未認証の攻撃者が Workhorse の高速アップロードハンドオフをバイパスし、攻撃者が制御する file.path を持つ 3 つのアップロードエンドポイントに到達でき、GitLab ホスト上で 任意ファイル読み取り が可能になります。
files の 1 文字を %66iles としてエンコードすると、Workhorse のアップロードルートは マッチしなくなり (Workhorse は エンコードされた パスでマッチする)、一方 Rails はそれをデコードして実際のハンドラに到達します (Rails は デコードされた パスでルーティングする)。ハンドラは Workhorse が上書きするはずだった生の file.path パラメータを信頼するため、file.path=/etc/passwd が認証情報なしでディスクから読み取られます。
POST /api/v4/projects/1/repository/%66iles/x?file=&file.path=/etc/passwd&file.size=1
^^^^^^ Workhorse misses -> raw file.path survives to Rails
| バージョン | |
|---|---|
| 影響を受ける | CE/EE 18.7 → 19.1.8, 19.2 → 19.2.6, 19.3 → 19.3.2 |
| 修正済み | 19.1.8 / 19.2.6 / 19.3.2 (2026-09-10) |
| パス | 内容 |
|---|---|
docs/ANALYSIS.md | 完全な根本原因分析 — パーサー差異、2 つの Workhorse JWT、生の file.path 信頼バグ、パッチ差分、およびリフレクトエラーによる外部流出チャネル。実際のソース (v19.3.1-ee 対 v19.3.2-ee) に対して分析を実施。 |
lab/ | Docker ベースの脆弱なラボ (gitlab-ce:19.3.1-ce.0) + 起動手順。 |
poc/ | detect.sh (非破壊的な存在オラクル) と exploit.sh (リフレクトエラーによるファイル読み取り)。単一ターゲット、認可ゲート付き。 |
GitLab の内部に詳しくない方へ。リクエストが通過する順序で、各レイヤーが何をするかを以下に示します。主要概念を含むより詳細な用語集は docs/ANALYSIS.md §0 にあります。
Internet
│
▼
┌────────┐ ┌───────────┐ ┌────────┐ ┌──────────────────────┐
│ NGINX │────▶│ Workhorse │────▶│ Puma │────▶│ Grape / Rails app │
│(proxy) │ │ (Go) │ │ (Ruby) │ │ (Ruby) │
└────────┘ └───────────┘ └────────┘ └──────────────────────┘
| コンポーネント | 概要 |
|---|---|
| NGINX | 最外層のリバースプロキシ。TLS を終端し、静的ファイルを配信し、それ以外をすべて内側に転送します。この脆弱性には直接関与しません。 |
| Workhorse | GitLab 固有の Go リバースプロキシ。主な役割は、Ruby が苦手とする処理 — 特に 大容量アップロードのストリーミング — をオフロードすることです。アップロードエンドポイントでは、Workhorse がボディを一時ファイルにバッファリングし、JWT に署名し、file.path を書き換えるため、Ruby が生のアップロードバイトを目にすることはありません。また、転送する すべての リクエストにリクエストごとの Gitlab-Workhorse-Api-Request JWT を付与します (これは「プロキシを通過した」ことを証明するものであり、「このユーザーが認証済みである」ことを証明するものでは ありません)。 |
| Puma | Rails アプリを実行する Ruby アプリケーションサーバー。Workhorse からリクエストを受け取り、ミドルウェア (Rack) を実行し、ルーターにディスパッチします。 |
| Rack | Ruby の Web サーバーインターフェース層。Rack ミドルウェアはクエリ文字列の解析、セッション管理、そして — ここで決定的に重要なのは — Workhorse のアップロード JWT の検証と UploadedFile オブジェクトの構築を処理します。Rack::Utils.parse_nested_query は、このエクスプロイトでファイル内容を漏洩させるエラーメッセージを生成する関数です。 |
| Grape | GitLab がすべての /api/v4/* エンドポイントで使用する REST API フレームワーク。ルート定義と、require_gitlab_workhorse! (プロキシチェック) や authenticate! (ユーザー ID チェック) などの before フィルタを提供します。Rails 上、Puma 上、Workhorse の背後で実行されるため、デコードされた URL パスを認識します。 |
| Rails | 全体的な Web フレームワーク (Ruby on Rails)。GitLab は Rails モノリスであり、モデル、サービス、ミドルウェアはすべて Puma 内のここで実行されます。 |
この脆弱性は、Workhorse (エンコードされた パスでルートをマッチする) と Grape/Puma (デコードされた パスでルーティングする) の間のギャップに存在します。以下を参照してください。
r.URL.EscapedPath() (エンコード済み) でルートをマッチしますが、Puma/Grape はデコードされたパスでルーティングします。Workhorse にとって %66iles ≠ 正規表現 files ですが、Rails にとっては files にデコードされます。file.path を署名済み一時パスに書き換えず、Gitlab-Workhorse-Multipart-Fields ヘッダーも設定しません — しかし、それでもリクエストを プロキシします (有効な Gitlab-Workhorse-Api-Request JWT 付きで)。authenticate! がなく、ハンドラは Workhorse が検証しパスを制限した params[:file] UploadedFile ではなく、params['file.path'] (攻撃者の文字列) を直接読み取っていました。Rack::Utils.parse_nested_query のエラー文字列 ("invalid %-encoding (<file bytes>)") を通じて漏洩します — レスポンスは最初の無効な % までのファイルバイトをリフレクトします。パッチは authenticate! を追加し、検証済みの UploadedFile に切り替え、e.message のリフレクトを停止します。docs/ANALYSIS.md §5 を参照してください。
# 1. Stand up the vulnerable lab (see lab/README.md for details)
cd lab && docker compose up -d # wait ~5 min for GitLab to become healthy
# 2. Non-destructive detection
../poc/detect.sh http://localhost:8929
# 3. File-read PoC against a file you're authorized to read on your own lab
../poc/exploit.sh http://localhost:8929 /var/opt/gitlab/gitlab-rails/etc/gitlab.yml
これは 防御および教育目的 で公開されています。すなわち、CISA KEV リストに掲載されている、開示済みでパッチ適用済みの CVE を理解し、検出し、パッチを適用するためです。ここにあるスクリプトは 単一ターゲット であり、ターゲットを明示的に指定する必要があります。
localhost で実行するように設計されています。サードパーティのホストに向けないでください。GitLab を運用している場合は、修正済みバージョンにアップグレードしてください — それが唯一の真の修復策です。
gitlab-org/gitlab @ v19.3.1-ee 対 v19.3.2-eeMIT — 分析および PoC コードのみ。GitLab は GitLab Inc. の商標です。このリポジトリは GitLab Inc. と提携しておらず、承認も受けていません。