12k
All articles

ユーザー生成コンテンツにおけるXSS攻撃の防止

ユーザー生成コンテンツのXSS攻撃を、許可リスト検証・出力エンコード・DOMPurifyを用いてReact、Vue、Angularアプリケーションで防ぐ方法を解説する。

OpenReplay Team
OpenReplay Team
ユーザー生成コンテンツにおけるXSS攻撃の防止

ユーザー生成コンテンツを通じたクロスサイトスクリプティング(XSS)攻撃は、Webアプリケーションが直面する最も持続的なセキュリティ脅威の一つです。コメントシステムの構築、フォーム送信の処理、リッチテキストエディタの実装など、ユーザー入力を受け入れて表示するあらゆる機能は、潜在的なXSS脆弱性を生み出します。モダンなJavaScriptフレームワークは組み込みの保護機能を提供していますが、それらのエスケープハッチや実際のアプリケーションの複雑さにより、開発者は適切なXSS防止技術を理解し実装する必要があります。

本記事では、ユーザー生成コンテンツにおけるXSSを防ぐための重要な戦略について説明します:入力検証と正規化、コンテキスト対応出力エンコーディング、リッチコンテンツの安全な処理、そして補完的な多層防御制御です。なぜ許可リスト検証がブロックリストフィルタリングに勝るのか、そしてフレームワークのデフォルトを活用しながら一般的なセキュリティの落とし穴を回避する方法を学びます。

重要なポイント

  • 常にブロックリストではなく許可リストを使用してユーザー入力を検証する
  • 各出力コンテキスト(HTML、JavaScript、CSS、URL)に適切なエンコーディング方法を適用する
  • DOMPurifyまたは類似のライブラリを使用してリッチHTMLコンテンツをサニタイズする
  • フレームワークのデフォルトを活用し、絶対に必要でない限りエスケープハッチを避ける
  • CSPヘッダーとセキュアなCookie属性による多層防御を実装する
  • 自動セキュリティテストでXSS防止対策をテストする

ユーザー生成コンテンツにおけるXSSリスクの理解

ユーザー生成コンテンツは、信頼できない入力と動的で対話的な機能の必要性を組み合わせるため、独特のXSS課題を提示します。コメントシステム、ユーザープロフィール、製品レビュー、協調編集ツールはすべて、悪意のあるスクリプト実行を防ぎながらHTMLライクなコンテンツを受け入れる必要があります。

React、Angular、Vue.jsなどのモダンフレームワークは、テンプレートシステムを通じて基本的なXSS防止を自動的に処理します。しかし、開発者がフレームワークのエスケープハッチを使用する際に、これらの保護は破綻します:

  • ReactのdangerouslySetInnerHTML
  • AngularのbypassSecurityTrustAs*メソッド
  • VueのV-htmlディレクティブ
  • innerHTMLを使用した直接的なDOM操作

これらの機能は正当な理由で存在します—フォーマットされたコンテンツの表示、サードパーティウィジェットの統合、ユーザーが作成したHTMLのレンダリングなど。しかし、各バイパスは慎重な処理を必要とする潜在的なXSSベクターを作成します。

入力検証:最初の防御線

許可リスト検証の実装

許可リスト検証は、受け入れ可能な入力を正確に定義し、デフォルトで他のすべてを拒否します。このアプローチは、既知の危険なパターンをブロックしようとするブロックリストフィルタリングよりもはるかに安全であることが証明されています。

メールアドレス、電話番号、郵便番号などの構造化データには、厳密な正規表現を使用します:

// 米国郵便番号の許可リスト検証
const zipPattern = /^\d{5}(-\d{4})?$/;

function validateZipCode(input) {
  if (!zipPattern.test(input)) {
    throw new Error('Invalid ZIP code format');
  }
  return input;
}

ブロックリストフィルタが失敗する理由

<>scriptタグなどの危険な文字をフィルタリングしようとするブロックリストアプローチは、必然的に失敗します:

  1. 攻撃者はエンコーディング、大文字小文字の変化、またはブラウザの癖を使用してフィルタを簡単に回避する
  2. 正当なコンテンツがブロックされる(アポストロフィをフィルタリングする際の「O’Brien」など)
  3. 新しい攻撃ベクターがブロックリストの更新よりも速く出現する

Unicodeと自由形式テキストの正規化

自由形式テキストを含むユーザー生成コンテンツについては、エンコーディングベースの攻撃を防ぐためにUnicode正規化を実装します:

function normalizeUserInput(text) {
  // NFC形式に正規化
  return text.normalize('NFC')
    // ゼロ幅文字を削除
    .replace(/[\u200B-\u200D\uFEFF]/g, '')
    // 空白をトリム
    .trim();
}

自由形式テキストを検証する際は、特定の危険な文字をブロックしようとするのではなく、文字カテゴリの許可リストを使用します。このアプローチは、セキュリティを維持しながら国際的なコンテンツをサポートします。

コンテキスト対応出力エンコーディング

出力エンコーディングは、ユーザーデータを表示用の安全な形式に変換します。重要な洞察:異なるコンテキストには異なるエンコーディング戦略が必要です。

HTMLコンテキストエンコーディング

HTMLタグ間でユーザーコンテンツを表示する際は、HTMLエンティティエンコーディングを使用します:

function encodeHTML(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// 安全:ユーザーコンテンツはエンコードされている
const userComment = "<script>alert('XSS')</script>";
element.innerHTML = `<p>${encodeHTML(userComment)}</p>`;
// 次のようにレンダリング: <p>&lt;script&gt;alert('XSS')&lt;/script&gt;</p>

JavaScriptコンテキストエンコーディング

JavaScriptコンテキストに配置される変数には16進エンコーディングが必要です:

function encodeJS(str) {
  return str.replace(/[^\w\s]/gi, (char) => {
    const hex = char.charCodeAt(0).toString(16);
    return '\\x' + (hex.length < 2 ? '0' + hex : hex);
  });
}

// 安全:特殊文字は16進エンコードされている
const userData = "'; alert('XSS'); //";
const script = `<script>var userName = '${encodeJS(userData)}';</script>`;

CSSコンテキストエンコーディング

CSS内のユーザーデータにはCSS固有のエンコーディングが必要です:

function encodeCSS(str) {
  return str.replace(/[^\w\s]/gi, (char) => {
    return '\\' + char.charCodeAt(0).toString(16) + ' ';
  });
}

// 安全:CSSエンコーディングがインジェクションを防ぐ
const userColor = "red; background: url(javascript:alert('XSS'))";
element.style.cssText = `color: ${encodeCSS(userColor)}`;

URLコンテキストエンコーディング

ユーザーデータを含むURLにはパーセントエンコーディングが必要です:

// URLパラメータには組み込みエンコーディングを使用
const userSearch = "<script>alert('XSS')</script>";
const safeURL = `/search?q=${encodeURIComponent(userSearch)}`;

リッチコンテンツの安全な処理

多くのアプリケーションでは、ユーザーからリッチHTMLコンテンツを受け入れる必要があります—ブログ投稿、製品説明、フォーマットされたコメントなど。単純なエンコーディングではフォーマットが破損するため、HTMLサニタイゼーションが必要です。

HTMLサニタイゼーションにDOMPurifyを使用

DOMPurifyは、安全なフォーマットを保持しながら危険な要素を削除する堅牢なHTMLサニタイゼーションを提供します:

import DOMPurify from 'dompurify';

// ニーズに合わせてDOMPurifyを設定
const clean = DOMPurify.sanitize(userHTML, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href', 'title'],
  ALLOW_DATA_ATTR: false
});

// サニタイズされたHTMLの挿入は安全
element.innerHTML = clean;

フレームワーク固有の安全なパターン

各フレームワークには、ユーザー生成コンテンツを安全に処理するための推奨パターンがあります:

React:

import DOMPurify from 'dompurify';

function Comment({ userContent }) {
  const sanitized = DOMPurify.sanitize(userContent);
  return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
}

Vue.js:

<template>
  <div v-html="sanitizedContent"></div>
</template>

<script>
import DOMPurify from 'dompurify';

export default {
  computed: {
    sanitizedContent() {
      return DOMPurify.sanitize(this.userContent);
    }
  }
}
</script>

Angular:

import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import DOMPurify from 'dompurify';

export class CommentComponent {
  constructor(private sanitizer: DomSanitizer) {}
  
  getSafeContent(content: string): SafeHtml {
    const clean = DOMPurify.sanitize(content);
    return this.sanitizer.bypassSecurityTrustHtml(clean);
  }
}

多層防御制御

適切なエンコーディングとサニタイゼーションが主要な保護を提供する一方で、追加の制御がセキュリティレイヤーを追加します:

コンテンツセキュリティポリシー(CSP)

CSPヘッダーは実行可能なスクリプトを制限し、XSSに対するセーフティネットを提供します:

// Express.jsの例
app.use((req, res, next) => {
  res.setHeader(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'nonce-" + generateNonce() + "'"
  );
  next();
});

セキュアなCookie属性

CookieにHttpOnlyとSecureフラグを設定してXSSの影響を制限します:

res.cookie('session', sessionId, {
  httpOnly: true,  // JavaScript アクセスを防ぐ
  secure: true,    // HTTPS のみ
  sameSite: 'strict'
});

テストと検証

XSS脆弱性をキャッチするための自動テストを実装します:

// Jestテストの例
describe('XSS Prevention', () => {
  test('should encode HTML in comments', () => {
    const malicious = '<script>alert("XSS")</script>';
    const result = renderComment(malicious);
    expect(result).not.toContain('<script>');
    expect(result).toContain('&lt;script&gt;');
  });
});

まとめ

ユーザー生成コンテンツにおけるXSSの防止には、多層アプローチが必要です。許可リスト入力検証と正規化から始め、データが表示される場所に基づいてコンテキスト対応出力エンコーディングを適用し、リッチコンテンツのサニタイゼーションにはDOMPurifyなどの実績のあるライブラリを使用します。モダンフレームワークは優れたデフォルト保護を提供しますが、それらのエスケープハッチをいつどのように安全に使用するかを理解することは依然として重要です。ブロックリストフィルタリングだけでは決して適切な保護を提供できないことを覚えておいてください—可能なすべての攻撃パターンをブロックしようとするのではなく、許可されるものを定義することに焦点を当ててください。

よくある質問

フォーマット付きHTMLコメントの投稿をユーザーに許可する際にXSSを防ぐにはどうすればよいですか?

DOMPurifyなどの適切にメンテナンスされたHTMLサニタイゼーションライブラリを使用してください。b、i、em、strong、a、pなどの安全なタグのみを許可し、scriptタグ、イベントハンドラ、危険な属性を除去するように設定します。多層防御のため、クライアント側だけでなくサーバー側でも常にサニタイズしてください。

ユーザー入力をデータベースに保存する際にエンコードすべきか、表示する際にエンコードすべきか?

ユーザー入力は元の形式でデータベースに保存し、出力時点でエンコードしてください。このアプローチは元のデータを保持し、後でエンコーディング戦略を変更できるようにし、各出力コンテキストに適切なエンコーディングを適用することを保証します。

ユーザー生成コンテンツのエスケープとサニタイズの違いは何ですか?

エスケープはすべてのHTMLタグをエンティティ等価物に変換し、実行するのではなくテキストとして表示します。サニタイズは安全なHTMLフォーマットを保持しながら危険な要素を削除します。プレーンテキストフィールドにはエスケープを、リッチコンテンツエディタにはサニタイズを使用してください。

XSS攻撃を防ぐMarkdownエディタを安全に実装するにはどうすればよいですか?

セキュアなライブラリを使用してサーバー側でMarkdownを解析し、結果のHTMLをクライアントに送信する前にDOMPurifyでサニタイズしてください。攻撃者がAPIに悪意のあるHTMLを直接送信してバイパスできるため、クライアント側のMarkdown解析だけに頼ってはいけません。

ReactなどのモダンJavaScriptフレームワークは自動的にすべてのXSS攻撃を防ぎますか?

モダンフレームワークは自動エスケープを通じてデフォルトでXSSを防ぎますが、これらの保護をバイパスするdangerouslySetInnerHTMLなどのエスケープハッチを提供しています。これらの機能を使用する際、ユーザーがアップロードしたファイルを処理する際、またはURLやCSS値を動的に構築する際には、手動で安全性を確保する必要があります。

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.