
Root-cause分析、脆弱なDockerラボ、およびCVE-2026-85706(パーサー差異を利用したGitLabの未認証任意ファイル読み取り)の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. と提携しておらず、承認も受けていません。