FlawOpen

flawopen.com/Teardowns/cve-2026-21589-atlassian-arbitrary-file-access

● CVE-2026-21589 · CVSS v4 9.3 · 緊急
セキュリティ研究 · FlawOpen

CVE-2026-21589:Atlassian Data Centerのファイルアクセス

Atlassian のアドバイザリと10月12日の MS-ISAC 報告はリスクと緊急性を示しますが、特定顧客のインスタンスでの悪用を証明するものではありません。[1], [4], [5], [10]

💡 わかりやすい解説 (ELI5)

来館者が指定した棚の場所を受け付ける図書館を想像してください。すべての棚を見て回ることはできませんが、特定の本の場所を知っていれば、境界確認の不備によって職員が閲覧エリア外から本を持ってくることがあります。修正では、解決されたパスを許可範囲内に保ち、製品ごとの更新を適用します。[1], [4]

主要な概念と専門用語

任意ファイルアクセス
想定された範囲外のファイルにリクエストからアクセスできる状態です。Atlassianが説明するのはWebアプリのルート内にある特定ファイルであり、ファイルシステム全体ではありません。[1], [4]
パストラバーサル
パス要素が想定されたディレクトリ境界を越える問題です。Atlassianの緩和策はURL内のトラバーサル表記とエンコード形式を対象にします。[1], [4]
Data Center
Atlassian製品のセルフマネージド展開モデルです。アドバイザリは8製品を対象とし、AtlassianはCloud製品は修正済みとしています。[1]
既知パスの前提
対象ファイルの正確な名前とパスを事前に知る必要があります。アドバイザリによると、ディレクトリの列挙はできません。[1]
CVSS 4.0
Atlassianは独自評価で本脆弱性をCritical、CVSS 4.0で9.3としています。[1]
Router.unescapeSlashes()
共有ライブラリはリソース処理前にエンコードされた区切り::を/へ戻します。watchTowrのソース分析ではこのデコード境界が重要です。[1], [4]
ResourceServingHelpers
ヘルパーがリソース名を解決し、残りの相対パスをcreateResourceWithRelativePath()へ渡します。[1], [4]
Crowd認証情報の連鎖
設定に依存する後続段階です。読み取り可能なCrowdプロパティからアプリケーションパスワードが漏れる可能性があり、利用可否はネットワーク到達性と権限次第です。[4], [5]
CWE-22
パストラバーサルの弱点に対するFlawOpenの編集上の分類です。パスが意図したディレクトリ内に十分制限されていません。AtlassianのアドバイザリはCWEを指定していません。[6]

フレームワーク対応

  • FlawOpenによるMITRE CWEへの概念上の対応付けです。ベンダーやCVEレコードがこの研究デモに弱点IDを割り当てたものではありません。 CWE-22

根本原因の分析 (Root Cause)

watchTowrによる共有ライブラリatlassian-plugins-webresourceの分析は、パスデコード境界を示しています。Router.unescapeSlashes()は::を/へ変換し、リソースルートはリクエスト制御可能なパス要素にこの関数を適用してからリソース処理を呼び出します。後段の検索はリソース名を分割し、残りの相対パスをcreateResourceWithRelativePath()へ渡します。代替の区切り表現により、トラバーサル検査の前提を回避できます。研究者はJira、Confluence、BitbucketでTomcatアプリケーションコンテキスト内のファイル読み取りを実証し、Atlassianは8製品を影響対象としています。報告によると、検証したパスはTomcatコンテキスト外へは出ませんでした。条件がそろうと、読み取り可能なWEB-INF/classes/crowd.propertiesからCrowdアプリケーションパスワードが平文で露出する可能性があります。影響は設定とCrowdへの到達性に依存します。BleepingComputerは、技術報告とPoC公開から数時間後にPrevidianセンサーが試行を観測したと報じています。これはハニーポットへの試行であり、顧客システムでの成功を示すものではありません。以下の脆弱なJava断片はwatchTowrの報告に基づきます。安全側は一般的な境界検証例で、未公開のAtlassian修正コードではありません。各製品の公式修正版を適用してください。[1], [4], [5]

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

ステップ 1

共有デコーダーがパスを変換

AtlassianのWebリソースルーターはリソースを返す前に代替区切り::を/へ変換します。通常のリソースルーティングで行われる処理です。[1], [4]

ステップ 2

リソース検索にデコード済みパスが渡る

ヘルパーがパス要素を分割し、残りの相対パスをリソースファクトリーへ渡します。代替区切りによりトラバーサル検査の想定を回避できます。[4]

ステップ 3

アプリケーションコンテキスト内のファイルが読める

watchTowrはJira、Confluence、Bitbucketで実証しました。報告ではテストパスはTomcatコンテキスト外へ出ていません。ホスト上の任意ファイルへ拡大解釈しないでください。[1], [4]

ステップ 4

ID管理への連鎖は設定次第

読み取り可能なCrowd設定にアプリケーションパスワードがあり、Crowdへ到達してその資格情報が受け入れられる場合、ID管理機能にアクセスされるおそれがあります。[4], [5]

ステップ 5

観測された試行は緊急度を高めるが侵害の証明ではない

10月12日、Center for Internet Security の MS-ISAC アドバイザリは、CVE-2026-21589 が実環境で悪用されたとの報告があると伝えました。被害者を特定せず、インシデントの証拠資料も公開していないため、MS-ISACによる悪用報告として扱い、特定顧客へのアクセス確認とは区別してください。10月5日の Atlassian アドバイザリでは、同社の調査で Cloud 製品の悪用証拠は見つかっておらず、Cloud 製品には修正が適用済みとされていました。この調査結果は Cloud を対象としており、脆弱性の悪用報告を伝えた MS-ISAC の発表とは矛盾しません。いずれの声明も、特定のインスタンスで何が起きたかを確定するものではありません。[1], [10]

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

脆弱なコード断片は、watchTowr が公開した対象 web-resource ライブラリのソースコード上のパス変換を示すもので、攻撃リクエストではありません。下段は Java の一般的な防御例であり、Atlassian のパッチではありません。信頼できるルートから SecureDirectoryStream のハンドルで順にたどり、各要素のシンボリックリンクを拒否し、読み取りバイト数を制限します。呼び出し側は一度だけデコードした相対パスと、信頼できるディレクトリをルートとして開いた SecureDirectoryStream を渡す必要があります。SecureDirectoryStream に対応しないファイルシステムプロバイダーもあるため、その場合は安全側に失敗させてください。製品ごとのベンダー修正版を適用してください。この例は、信頼できるリソースツリーを前提とします。ディレクトリやファイルを信頼できない主体が変更できず、通常ファイルだけを含む必要があります。コード自体はファイル種別を検証しません。バイト上限と、進行しない読み取りへの回数制限は読み取りを制限しますが、汎用的なファイル配信処理ではありません。呼び出し側は一度だけデコードした相対パスを渡し、信頼できるルートストリームを開いたまま保持する必要があります。ファイルシステムプロバイダーは SecureDirectoryStream をサポートしない場合があるため、利用できなければ安全側に失敗してください。[4], [7]

脆弱な実装
public static String unescapeSlashes(String string) {
    return string.replaceAll("::", "/");
}
堅牢化されたセキュアパッチ
static byte[] readUnder(SecureDirectoryStream<Path> root, Path relative, int maxBytes) throws IOException {
    if (relative.isAbsolute() || relative.getNameCount() == 0 || maxBytes < 0) {
        throw new SecurityException("Expected a relative path and non-negative limit");
    }
    List<SecureDirectoryStream<Path>> opened = new ArrayList<>();
    SecureDirectoryStream<Path> parent = root;
    try {
        for (int i = 0; i < relative.getNameCount() - 1; i++) {
            Path part = relative.getName(i);
            if (part.toString().isEmpty() || part.toString().equals(".") || part.toString().equals("..")) {
                throw new SecurityException("Invalid path component");
            }
            parent = parent.newDirectoryStream(part, LinkOption.NOFOLLOW_LINKS);
            opened.add(parent);
        }
        Path leaf = relative.getFileName();
        if (leaf.toString().isEmpty() || leaf.toString().equals(".") || leaf.toString().equals("..")) {
            throw new SecurityException("Invalid file name");
        }
        try (SeekableByteChannel channel = parent.newByteChannel(
                leaf, Set.of(StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS))) {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            int total = 0;
            int zeroReads = 0;
            while (true) {
                int count = channel.read(buffer);
                if (count == -1) break;
                if (count == 0) {
                    if (++zeroReads >= 3) throw new IOException("Resource read made no progress");
                    continue;
                }
                zeroReads = 0;
                if (count > maxBytes - total) throw new IOException("Resource exceeds size limit");
                out.write(buffer.array(), 0, count);
                total += count;
                buffer.clear();
            }
            return out.toByteArray();
        }
    } finally {
        for (int i = opened.size() - 1; i >= 0; i--) opened.get(i).close();
    }
}

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

参考資料