
低権限のDocmostユーザーが、被害者のattachmentIdを汎用アップロードエンドポイントに指定し、同じワークスペース内の別のページに保存された添付ファイルを上書きする可能性があります。
低権限のDocmostユーザーが、攻撃対象のattachmentIdを汎用アップロードエンドポイントに送り、同じワークスペース内の別ページの保存済み添付ファイルを上書きできる可能性があります。
私は、オープンソースのコラボレーションドキュメントプラットフォーム Docmost において、高重要度の認可の欠陥を特定し、責任を持って開示し、再現しました。
Docmostの公式サイトは、これをエンタープライズ対応のオンプレミスWikiとして紹介しており、300万以上のダウンロードがあり、ビリニュス市、Bechtle、オーストラリア政府、赤十字、ETS Quebecなどの組織のチームから信頼されていると述べています。
このバグは、Docmostが図の保存/更新フローでも使用する汎用ファイルアップロードパスに存在していました。
私はそのコードを非常に具体的な質問を念頭に置いてレビューしていました。
アップロードエンドポイントがあるページの編集アクセスを証明した場合、上書き対象が別のユーザー制御の添付ファイルIDで選択された場合はどうなるのか?
このケースでは、その質問が直接、実際のオブジェクトバインディングの失敗につながりました。
Docmostは、呼び出し元が次のものを送信することを許可していました。
pageId、およびattachmentIdサーバーは上書きの整合性チェックを実行しましたが、ガードは間違ったブール論理を使用していました。
つまり、リクエストは認可を通過し、それでも被害者の添付ファイルを上書きできたということです。
この問題は CVE-2026-34213 になりました。
Docmost: docmost/docmost
アドバイザリ: GHSA-89fp-2hch-j9gp
CVE: CVE-2026-34213
パッチ適用バージョン: v0.71.0

攻撃者制御のpageId(編集アクセスあり) -> 攻撃者制御の被害者attachmentId -> 欠陥のある上書きガードがクロスページ上書きを有効として扱う -> 被害者attachmentIdからストレージパスが再構築される -> 攻撃者のバイトが被害者ファイルを置き換える -> 被害者ページは変更された添付ファイルを引き続き提供する
Docmostは、アップロードされたページ添付ファイルをデータベースレコードとストレージ内のバックアップファイルとして保存します。
通常のアップロードでは、サーバーは新しい添付ファイルIDを作成し、新しいファイルを書き込みます。
ただし、図の保存/更新フローでは、クライアントは既存のattachmentIdを意図的に再利用して、毎回新しい添付ファイルレコードを生成するのではなく、同じ図ファイルをその場で更新できるようにします。
その動作自体は正当です。
問題は、それが高リスクのパスを生み出すことです。
エンドポイントがこれら2つの責任を混在させるときはいつでも、実装はそれらを正確にバインドする必要があります。
Docmostはそうしませんでした。
作成/更新の混在エンドポイントは、認可バグが発生しやすい場所です。
その理由は単純です。
まさにそれがここでのパターンです。
POST /api/files/upload は、呼び出し元がpageIdで指定されたページを編集できることを検証しました。
しかし、attachmentIdも提供された場合、サーバーは上書きパスに切り替わり、既存の添付ファイルレコードを別途選択しました。
これにより、重要なセキュリティ上の疑問が生じました。
上書きパスは、選択された添付ファイルが実際に認可されたページに属することを証明するか?
脆弱なバージョンでの答えは「いいえ」でした。
根本原因は、ユーザー制御のキーによる認可バイパスと、上書きガードのブール論理バグの組み合わせでした。
脆弱なフローは次のようになりました。
AttachmentController.uploadFile() はマルチパートフォームデータからpageIdを読み取りました。validateCanEdit(page, user) を呼び出しました。attachmentIdを別途受け入れました。AttachmentService.uploadFile() は、攻撃者によって提供されたIDで既存の添付ファイルをロードしました。&&を使用しました。脆弱なガードは次のとおりです。
if (
existingAttachment.pageId !== pageId &&
existingAttachment.fileExt !== preparedFile.fileExtension &&
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
その条件は、次の場合にのみリクエストを拒否しました。
すべて同時に。
それは、上書きガードがすべきことの逆です。
実際の攻撃ケースでは、攻撃者は意図的に同じワークスペース内に留まりました。
つまり:
existingAttachment.workspaceId !== workspaceId は false でしたそのオペランドがfalseになると、添付ファイルが別のページに属していても、&&条件全体がfalseと評価されました。
そのため、サーバーはクロスページ上書きを有効として扱いました。
それがバグの前半です。
後半は、影響を現実のものにした部分です。
チェックの後、サービスは攻撃者提供のattachmentIdとファイル名を使用して宛先ストレージパスを再構築しました。
const filePath =
`${getAttachmentFolderPath(AttachmentType.File, workspaceId)}/` +
`${attachmentId}/${preparedFile.fileName}`;
その後、更新パスでは、DocmostはfileSizeやupdatedAtなどの可変メタデータのみを更新しました。
所有権を攻撃者ページに再バインドすることはしませんでした。
そのため、被害者ページは同じ添付ファイルレコードと添付ファイルIDを指し続けました。 変更されたのは、基になるファイルのバイトだけでした。
だからこそ、これは無害な不一致ではありませんでした。
これは、永続的な不正な上書きプリミティブでした。
これは表面的なバグでも、ファイル名の衝突問題でもありませんでした。
攻撃者はレースを必要としませんでした。 攻撃者はランダムなパスを推測する必要もありませんでした。 攻撃者は被害者ページへの書き込みアクセスを必要としませんでした。
必要なのは次のものだけでした。
そこから、攻撃者は別のページの添付ファイルの保存済みファイルバイトを置き換えることができ、被害者ページは何も変わっていないかのようにその添付ファイルを参照して提供し続けました。
これは直接的な整合性の失敗です。
現実的には、攻撃者は次のことが可能でした。
重要な点はこれです。
サーバーは、攻撃者が選択した上書きターゲットを受け入れ、実際に編集権限がチェックされたページにそれをバインドしませんでした。
これはアクセス制御の失敗であり、単なる悪いブール演習ではありません。
エクスプロイトは、特に図の添付ファイルに対して現実的でした。
Docmostのクライアントは、図の保存のために意図的にattachmentIdを再利用し、決定論的なファイル名を使用します。
diagram.excalidraw.svgdiagram.drawio.svgこれは重要です。なぜなら、攻撃者の要件を引き下げるからです。
一般的な添付ファイルの場合、攻撃者は両方を必要とします。
図の場合、ファイル名はすでに予測可能です。
そのため、攻撃者が被害者ページのコンテンツを読むことができれば、不足している唯一のピースを回復できることがよくあります。
attachmentId私の検証セットアップでは、まさにそのパスを使用しました。
それで十分でした。
エクスプロイトは、欠陥のあるワークスペースチェックを満たしながら、同じワークスペース内のページ境界とスペース境界を越えました。
私は**Docmost v0.70.3**に対して、docmost/docmost:0.70.3、Postgres、Redisから構築した使い捨てラボを使用して、問題をライブで検証しました。
PoCフローは次のとおりです。
attachmentIdをメモします。POST /api/files/uploadを送信します。
pageId = 攻撃者ページIDattachmentId = 被害者添付ファイルIDfile = 被害者ファイル名を使用した攻撃者制御の置換ファイル最小限のリクエスト形状は次のとおりです。
POST /api/files/upload
Content-Type: multipart/form-data
pageId=<attackerPageId>
attachmentId=<victimAttachmentId>
[email protected];filename=diagram.excalidraw.svg
観察されたライブの結果は次のとおりです。
019d18ae-b176-751c-8525-b5f3cede131d019d18ae-b15b-70e9-ac67-64948e87cc5e019d18ae-b12f-75ec-8c1c-5aff3ba6be9c200 OK686a0a0ede90ece1cbb975bb29304a6c3a90373a9c3ab2496345cf7ca59cc8fa
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
e0168298846cdaf75c4d880f4b721d7c0ef0ef310f75617bf2b833af34cdbeba
Attacker replacement from another page
これは、完全なエンドツーエンドの上書き証明であり、単なる理論的なソースレビューではありません。
トリアージ中に、私は2つのスタイルの証明を使用しました。
スタンドアロンハーネスは、ブール論理の失敗を分離するのに役立ちました。
ライブHTTP PoCは、完全なセキュリティストーリーを証明したため、より強力な成果物でした。
attachmentIdが受け入れられたこの区別は、アクセス制御バグでは重要です。
「条件が間違っている」だけでは十分ではありません。
「条件が間違っており、アプリケーションをエンドツーエンドで永続的な不正な上書きに駆動できる」というのが完全なケースです。
修正は**v0.71.0**で出荷され、上書きガードを&&から||に変更しました。
if (
existingAttachment.pageId !== pageId ||
existingAttachment.fileExt !== preparedFile.fileExtension ||
existingAttachment.workspaceId !== workspaceId
) {
throw new BadRequestException("File attachment does not match");
}
そのパッチは最小限で、直接的で、報告されたバグに対して正しいです。
正しいルールを復元します。
上書きは、既存の添付ファイルが認可されたページ/ワークスペース/タイプの前提と完全に一致する場合にのみ許可されます。
ガードが不一致を拒否するようになったため、次のことが行われます。
これは正しい種類の修正でした。
認可されたページと上書きターゲットの間の厳密なバインディングだけです。
それでも、ここにはより広範なエンジニアリングの教訓があります。
インプレース更新フローも提供する汎用アップロードエンドポイントは、高リスクのAPIサーフェスとして扱う必要があります。
当面のバグが修正されたとしても、より強力な長期的な設計は次のとおりです。
しかし、脆弱性自体については、公開されたパッチがコアの問題をきれいに閉じました。
プロジェクトが修正の周りに独自のプライベートテストを追加したかどうかに関係なく、長期的なカバレッジのために重要なケースは次のとおりです。
これらのテストのポイントは、単なる正確性ではありません。
それは、将来の「便利な」アップロードリファクタリングが同じクラスのバグを再び開かないように、認可バインディングをロックダウンすることです。
公開されたアドバイザリは、この問題を次のように分類しました。
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
これは7.1 / Highになり、正しい結論です。
ここで重要なメトリックはIntegrityです。
これは低グレードのメタデータバグではありませんでした。 攻撃者は、別のページの添付ファイルパスに書き込まれる置換バイトを完全に制御し、被害者ページはその後も変更されたオブジェクトを提供し続けました。
それはまさに、Integrity Highに値する、保存されたクロスレコード改ざんの種類です。
AvailabilityがLowのままであることも理にかなっています。図や添付ドキュメントを破損すると、被害者のコンテンツが使用できなくなる可能性がありますが、主な影響は完全なサービス中断ではなく、依然として不正な変更です。
私はGitHub Security Advisoriesを通じて、以下の内容で問題を非公開で報告しました。
問題はメンテナーに受け入れられ、CVE-2026-34213が割り当てられ、2026年4月14日に公開されました。
公開アドバイザリには次のように記載されています。
>= v0.3.0v0.71.0この履歴は、私のローカルソースレビューとも一致しました。 脆弱な上書きロジックは、脆弱な行でチェックした最初のタグ付けリリースに存在していました。
ここでの興味深い教訓は、単に「&&の代わりに||を使用する」ではありません。
それは症状です。
より深い教訓は次のとおりです。
1つのユーザー制御フィールドが認可を証明し、別のユーザー制御フィールドが更新されるオブジェクトを選択する場合、これら2つのフィールドは明示的かつ正確にバインドされなければなりません。
このルールはどこにでも現れます。
システムが次のように言う瞬間、
それは、正確な一致不変条件で強制されなければならないセキュリティ境界を作成しました。
それよりも緩いものは、遅かれ早かれユーザー制御キーのバグになります。
この問題は、過小評価されがちな2番目のポイントも強化します。
ガードコードの小さなブールミスは、ファーストクラスのセキュリティ結果をもたらす可能性があります。
一目で「妥当に見える」3句条件は、上書きパスの保護モデルを逆転させるのに十分でした。
そのため、これらのサーフェスは、カジュアルな信頼ではなく、意図的なレビューに値します。
pageIdに対してチェックされましたが、上書きターゲットの選択には別の呼び出し元が提供したattachmentIdが使用されました。v0.71.0の修正は、ガードを正しく変更して、不一致を拒否するようにしました。この脆弱性は、エキゾチックなストレージ動作に関するものではありませんでした。
それは、更新パスが攻撃者が選択したオブジェクト識別子を信頼しすぎたことに関するものでした。
Docmostはあるページの編集アクセスを証明し、別のページから既存の添付ファイルIDを受け入れ、欠陥のある上書きチェックによってその不一致を成功したクロスページファイルの置き換えに変えさせました。
だからこそ、それはCVE-2026-34213になりました。
v0.71.0のパッチは当面の問題をきれいに修正しましたが、より広範な教訓は依然として価値があります。
認可とオブジェクトの選択が別々のユーザー制御フィールドに分割されている場合、正確なバインディングがセキュリティプロパティです。