JHipster: 生成されたアプリケーションにおける、制限のないBlobコンテンツタイプによるXSS脆弱性

## 概要
generator-jhipster v9.2.0によって生成されたアプリケーションでは、ユーザーが制御するBlobのコンテンツタイプ値を永続化し、後でそれらの値をクライアント側のBlobオブジェクトのMIMEタイプとして使用することができます。共有された生成済みヘルパー関数`openFile`は、BlobからオブジェクトURLを作成し、新しいウィンドウでそれを開きます。

通常の認証ユーザーが書き込み可能なBlobを持つエンティティの場合、これにより、保存型のXSS攻撃の連鎖が発生します。攻撃者が制御するBlobの内容とMIMEタイプが、生成されたREST APIを通じて保存され、特権を持つユーザーに返され、生成されたAngular/React/VueのUIから、同一オリジンのBlobドキュメントとして開かれます。脆弱性の有無は、生成されたアプリケーションのCSP(コンテンツセキュリティポリシー)と、対象のブラウザに対して確認する必要があります。

## 詳細
根本的な原因は、生成されたサーバー側コードとクライアント側コード間の信頼境界の失敗です。生成されたエンティティのRESTリソースは、エンティティの作成と更新のためのリクエストボディを受け入れます。エンティティ固有の権限が設定されていない場合、テンプレートはメソッドレベルの`@PreAuthorize`を省略し、グローバルな`/api/**`セキュリティルールに基づいて、認証されたユーザーであれば誰でもエンドポイントにアクセスできるようになっています。

生成されたDTO/ドメイン/マッパーのフローでは、関連する`<field>ContentType`プロパティが単純な`String`として保存され、エンティティのBlobデータとともに永続化されます。関連するテンプレートには、`generators/spring-boot/templates/src/main/java/_package_/_entityPackage_/service/dto/_dtoClass_.java.ejs:96-122`と`generators/java/generators/domain/templates/src/main/java/_package_/_entityPackage_/domain/_persistClass_.java.jhi.ejs:124-140`が含まれます。生成されたコードでは、MIMEタイプの制限、アクティブなドキュメント形式の拒否、および宣言されたコンテンツタイプがアップロードされたバイトと一致するかどうかの検証が行われていません。

危険な箇所は、`generators/client/generators/common/templates/src/main/webapp/app/shared/jhipster/data-utils.ts.ejs`の49行から64行にある`openFile`関数です。この関数は、返された`contentType`をBlobの型として使用し、オブジェクトURLを作成し、`globalThis.open(fileURL)`を呼び出します。生成されたAngular、React、およびVueのエンティティページは、サーバーから返されたBlobデータとContentTypeをこのヘルパー関数に渡します。これにより、CSP(コンテンツセキュリティポリシー)とブラウザの動作によっては、アクティブなHTMLまたはSVGコンテンツがアプリケーションのオリジン下でドキュメントとして開かれる可能性があります。

脆弱なコードパス:

```typescript
// generators/client/generators/common/templates/src/main/webapp/app/shared/jhipster/data-utils.ts.ejs:49-64
export const openFile = (data: string, contentType: string | null | undefined): void => {
contentType ??= '';

const byteCharacters = atob(data);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.codePointAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
const blob = new Blob([byteArray], {
type: contentType,
});
const fileURL = globalThis.URL.createObjectURL(blob);
const win = globalThis.open(fileURL);
if (win) {
win.onload = () => URL.revokeObjectURL(fileURL);
```

共有クライアント側のこの関数は、受信した`contentType`を信頼し、その型でBlobを作成し、生成されたオブジェクトURLを通じてそれを開きます。したがって、アクティブなMIMEタイプは、開かれる際に実行可能なドキュメントになる可能性があります。これは、CSPとブラウザの動作に依存します。

## 実証
前提条件:対象アプリケーションは、generator-jhipster v9.2.0で生成されたもの。Blob、AnyBlob、またはImageBlobフィールドを含むエンティティが含まれている。エンティティにentityAuthorityによる制限がないか、または攻撃者が書き込み権限を持っている。攻撃者は通常のユーザーアカウントを持ち、より高い権限を持つ被害者がエンティティの詳細ページまたはリストページを閲覧できる。

再現手順:(1) Blobデータを含むエンティティを生成します。例えば、ペイロードとしてAnyBlobフィールドを持つDocumentエンティティなどです。(2) 通常のユーザーとして認証し、Authorization: Bearer <attacker-token> と Content-Type: application/json を指定して、POST /api/documents リクエストを送信します。リクエストのボディには、Base64エンコードされたアクティブなドキュメントをペイロードフィールドに含め、ペイロードのコンテンツタイプとして text/html などの値を設定します。(3) APIがHTTP 201を返し、GET /api/documents/{id} が同じペイロードとペイロードのコンテンツタイプを返すことを確認します。(4) 特権ユーザーが生成されたエンティティの詳細ページまたはリストページを開き、Blobフィールドの「開く」リンクをクリックします。期待される結果:ブラウザが blob:<target-origin>/... というドキュメントを開きます。CSP(コンテンツセキュリティポリシー)とブラウザの動作がスクリプトの実行を許可する場合、そのドキュメントは、Web Storageのトークンを読み取ったり、/api/account や特権的な /api/admin/** エンドポイントなど、同一オリジンのAPIを被害者のセッションで実行したりする可能性があります。

影響:
通常の認証済みユーザーは、特権ユーザーが生成されたUIから開く可能性のあるアクティブなコンテンツを永続化できます。攻撃が成功した場合、保存型のクロスサイトスクリプティング、JWT/localStorage/sessionStorageの窃取、機密APIデータの漏洩、および被害者のセッションにおける特権的な操作につながる可能性があります。デフォルトのCSPによって攻撃の成功率が低下する可能性がありますが、CSPが緩和されている環境や、開かれたBlobドキュメント内でスクリプトの実行を許可するブラウザの動作がある環境では、リスクが最も高くなります。

影響を受けるパッケージ:
- npm (JavaScript) generator-jhipster: 影響を受けるバージョンは 9.4.0 未満、9.4.0 で修正済み
- npm (JavaScript) react-jhipster: 影響を受けるバージョンは 1.0.3 以下、1.1.0 で修正済み