Quasar Framework: `getHead()`関数における、エスケープ処理されていないSSRメタタグのレンダリングによる、格納型/反射型XSS脆弱性
## 脆弱性の詳細
**ファイル**: `ui/src/utils/meta/Meta.js` (実際は `ui/src/plugins/meta/Meta.js`) (149行目-176行目: `getAttr()`関数と`getHead()`関数)
**脆弱性箇所**: `injectServerMeta()`関数 (同じファイル) → `ctx.headTags += getHead(data)`。これは、プロダクション環境のSSRテンプレート (`app-vite/templates/entry/ssr-prod-webserver.js` + `app-vite/lib/plugins/vite.html.js`)によって、生のHTTPレスポンスの`<head>`セクションにそのまま挿入される。
**エントリポイント**: パブリックな`useMeta()`コンポーザブル (`ui/src/composables/use-meta/use-meta.js`)。これは、アプリケーションがページタイトル/メタデータ/リンク/スクリプトタグを設定する唯一の公式な方法である。
### 根本原因
`getHead()`関数は、QuasarのSSR環境でのみ使用されるシリアライザーであり、すべての`useMeta()`呼び出しから収集されたメタデータ/リンク/スクリプト/タイトル情報を、リテラルなHTML文字列に変換する。この際、プレーンなテンプレートリテラル補間を使用し、**HTMLエンティティのエスケープ処理と、属性値の引用符のエスケープ処理は一切行われない**:
```js
function getAttr(seed) {
return att => {
const val = seed[att]
return att + (val !== true && val !== void 0 ? `="${val}"` : '')
}
}
function getHead(meta) {
let output = ''
if (meta.title) {
output += `<title>${meta.title}</title>`
}
...
}
```
これとは対照的に、クライアントサイドの同等の機能である`apply()`関数 (同じファイル、ブラウザでのhydration後にのみ使用)は、`document.createElement(...)`と`tag.setAttribute(att, val)`を使用して同じタグを構築する。ブラウザのDOM APIは、属性値を自動的にエスケープするため、**クライアントサイドの処理には脆弱性はない**。`getHead()`は、SSR環境のために用意された、別の並行実装であり、同じ保護が施されていなかった。
`useMeta()`関数に渡される`title`、`meta.*.content`、`link.*.href`、またはその他の属性値に、`</title>`、`"`、または`>`を含む文字列が含まれている場合、その文字列は意図されたHTMLコンテキストから抜け出し、任意のマークアップを挿入する。これには、実行可能な`<script>`タグも含まれる。このマークアップは、すべての訪問者に送信される生のHTMLレスポンスに直接挿入される。
### 攻撃シナリオ
1. Quasar SSRアプリケーションは、標準的なドキュメント化された`useMeta()`パターンを使用して、動的なページメタデータをレンダリングします。例えば、ブログ/CMS/製品ページの`useMeta(() => ({ title: post.title, meta: { description: { name: 'description', content: post.excerpt } } }))`などです。
2. 攻撃者が、その基となるテキストに影響を与えられる場合(ブログ投稿/コメントを送信する、独自のプロフィール表示名を設定する、統合されたCMS/APIのフィールドを制御するなど)、そのテキストを`My Post</title><script>alert(document.cookie)</script>`に設定します。
3. そのページがSSRでレンダリングされるたびに(すべての訪問者に対して)、`getHead()`関数がこのペイロードをエスケープせずに、生のHTMLレスポンスの`<head>`要素に直接出力します。
4. ユーザーのブラウザはHTMLを上から下まで解析し、注入された`<script>`がすぐに実行されます。これは、hydration(初期表示)の前に実行され、`document.cookie`とDOMへの完全なアクセス権を持ちます。
### 影響
- **種類**: CWE-79 クロスサイトスクリプティング(XSS)
- **認証の必要性**: 不要(攻撃者は、`useMeta()`に到達するテキストに影響を与える必要があるだけであり、これは非常に一般的なパターンです。例えば、ブログのタイトル、製品名、ユーザーの表示名など)
- **結果**: 攻撃対象のサイトのオリジンにおける、クライアントサイドスクリプトの完全な実行。セッション/Cookieの窃取、認証情報のフィッシング詐欺、アカウントの乗っ取り、改ざんなど。 通常のXSSの脆弱性では、特定の異常な注入ポイントが必要ですが、この脆弱性は最も一般的な`useMeta()`の使用方法(動的なタイトル/説明のレンダリング)に影響するため、悪意のない意図で`&`、`<`、または`"`を含む通常のコンテンツでもトリガーされる可能性があります。また、意図的に簡単に悪用することも可能です。
### 脆弱なコード (`ui/src/plugins/meta/Meta.js` の149行目から176行目)
```js
function getAttr(seed) {
return att => {
const val = seed[att]
return att + (val !== true && val !== void 0 ? `="${val}"` : '')
}
}
```
```
function getHead(meta) {
let output = ''
if (meta.title) {
output += `<title>${meta.title}</title>`
}
;['meta', 'link', 'script'].forEach(type => {
const metaType = meta[type]
for (const att in metaType) {
const attrs = Object.keys(metaType[att])
.filter(item => item !== 'innerHTML')
.map(getAttr(metaType[att]))
output += `<${type} ${attrs.join(' ')} data-qmeta="${att}">`
if (type === 'script') {
output += (metaType[att].innerHTML || '') + '</script>'
}
}
})
return output
}
```
### 推奨される修正
```js
function escapeHtml(val) {
return String(val)
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
}
function getAttr(seed) {
return att => {
const val = seed[att]
return att + (val !== true && val !== void 0 ? `="${escapeHtml(val)}"` : '')
}
}
function getHead(meta) {
let output = ''
if (meta.title) {
output += `<title>${escapeHtml(meta.title)}</title>`
}
// ... その他の部分は変更なし。scriptのinnerHTMLは意図的にそのまま(JSON-LDの利用ケース)
}
```
### 検証
v2.21.1で、実際の変更されていない`Meta.js`ソースをインポートし、実際の`curl`リクエストを通じて到達する、実際のSSRアプリケーションが行う`useMeta()` → `injectServerMeta()`の呼び出しシーケンスを再現する、ローカルのNode.js HTTPテスト環境でエンドツーエンドで確認しました。
```
1. `POST /submit` に `{"title":"My Post</title><script>alert(document.cookie)</script>","description":"\"><script>alert(document.domain)</script>"}` を送信。
2. `GET /render/1` で返された HTTP レスポンスの `<head>` には、`<title>My Post</title><script>alert(document.cookie)</script></title><meta name="description" content=""><script>alert(document.domain)</script>" data-qmeta="description">` が含まれていた。
3. `grep` コマンドで確認した結果、`<` の出現は 0 回(エスケープ処理がされていない)、実際の HTTP レスポンスの本文には、`<script>alert(...)</script>` タグが 1 回、リテラルで記述されていた。
修正版のブランチ (`fix/xss-meta-tag-escaping`) が準備されており、上記の最小限の修正が含まれている(`getAttr()`/`getHead()` で使用される `escapeHtml()` ヘルパー関数を追加)。修正されたコードに対して同じ PoC (Proof of Concept) を再実行すると、ペイロードが完全に HTML エンティティに変換されている (`<script>...`) ため、実行可能な `<script>` タグは存在しない。通常のタイトルに含まれる `&` は、正しく表示される (`&`)。
影響を受けるパッケージ:
- npm (JavaScript) quasar: 影響を受けるバージョンは 2.22.0 未満、2.22.0 で修正済み。