FlawOpen

flawopen.com/Incidents/gitlab-commits-api-path-traversal-september-2026

● GitLab · CVE-2026-85706 · September 2026 · CVSS 10.0 · 緊急
セキュリティ研究 · FlawOpen

CVE-2026-85706:GitLabでのサーバーファイル露出

GitLabの未認証アップロード解析でサーバーファイルを選択でき、不正なエスケープによりエラーへ断片が現れる場合がありました。[1], [3]

CVSS ベクトルCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N[3]
💡 わかりやすい解説 (ELI5)

受付係が受取箱の書類ではなく、来訪者が紙に書いた書庫の場所を受け取ったとします。身分証を確認する前に書庫を開き、読めない一行があればエラーの説明で声に出してしまいます。開くことが最初の失敗で、内容を読み上げることが次の失敗です。[1]

主要な概念と専門用語

Upload metadata
要求側の file.path や metadata.path で、信頼できるアップロード処理が登録したバッファを置き換えてはいけません。[1]
Percent decoding
フォーム解析では % と続く二桁の十六進数を復号します。不正な並びは問題の断片を含むエラーを起こし得ます。[1]
CWE-22 / CVSS
CNA は CVE-2026-85706 をパストラバーサル、CVSS 10.0 の重大な問題に分類しています。越えた境界は要求側によるサーバーファイルの選択です。[3]

フレームワーク対応

インシデントのタイムライン

2026-09-10

GitLab は修正版 19.1.8、19.2.6、19.3.2 を公開しました。[2]

2026-09-23

公開情報に 18.11.12 と 19.0.9 への修正の移植が追記されました。[2]

ステップ・バイ・ステップの攻撃フロー

ステップ 1

認証前の選択

アップロードのメタデータにより、認証前に想定された一時バッファをサーバーのパスで置き換えられました。[1]

ステップ 2

内容をフォームとして解析

application/x-www-form-urlencoded では選択されたファイルがフォームデータとして扱われ、通常のコミット応答にはなりません。[1]

ステップ 3

エラーへの反映

不正なパーセントエスケープは該当断片をエラーに含め得ます。& と = が範囲を左右し、読み取りだけでは漏えいを証明できません。[1]

ステップ 4

調査

correlation_id で Rails API と Workhorse の記録を関連付け、api_error と written_bytes を比較して、応答でバイトが送信されたかを判断します。[1]

ソースコード比較:脆弱 vs 堅牢化

これは Python の教育用モデルで、GitLab のソースコードや修正ではありません。脆弱な例はメモリ内のバッファだけを使い、どんな % もエラーとみなします。実際の検証や断片の選択は再現しません。改善例は先に認証し、パスのメタデータを拒否し、信頼できる処理が登録したバイトだけを受け取り、サイズを制限して一般的なエラーを返します。製品更新も必要です。前段でファイルを読んだ後に経路内で認証しても手遅れです。[1]

脆弱な実装
from urllib.parse import unquote_to_bytes

def parse_upload(authenticated, metadata, buffers):
    # 無害なメモリ内フィクスチャで、ファイルシステムにはアクセスしません。
    body = buffers.get(metadata.get("path"), b"")
    if b"%" in body:
        return 400, body.decode("ascii", errors="replace")
    if not authenticated:
        return 401, "Unauthorized"
    return 200, unquote_to_bytes(body)
堅牢化されたセキュアパッチ
from urllib.parse import unquote_to_bytes

MAX_BYTES = 4096
HEX = frozenset(b"0123456789abcdefABCDEF")

def parse_upload(authenticated, metadata, registered_body):
    # registered_body は信頼できるアップロードミドルウェアから受け取り、
    # リクエストパラメーターで選ばれたパスからは取得しません。
    if not authenticated:
        return 401, "Unauthorized"
    if metadata or type(registered_body) is not bytes:
        return 400, "Invalid upload"
    if len(registered_body) > MAX_BYTES:
        return 413, "Upload too large"
    for i, byte in enumerate(registered_body):
        if byte == 37 and (i + 2 >= len(registered_body) or
                registered_body[i+1] not in HEX or
                registered_body[i+2] not in HEX):
            return 400, "Invalid upload"
    return 200, unquote_to_bytes(registered_body)

エンジニアリング&システム堅牢化チェックリスト

参考資料