HOWTO · PHP

PHP で入力をサニタイズする

今日の記事では、入力データをデータベースに渡す前に、PHP でサニタイズする方法を学習します。

このページの内容

すべての信頼できない値を安全にする万能な手順はありません。値が何を表すのかを判断し、その要件に照らして検証します。正規化するのは、アプリケーションに明確なルールがある場合に限ります。表示する際は出力先のコンテキストに合わせてエンコードし、データベースに送る値にはパラメーター化クエリを使います。これらの処理はそれぞれ異なる問題を解決します。検証は値を受け入れるか拒否し、サニタイズは値を変換し、出力エンコードは特定のコンテキストで文字を安全に表現します。どの処理も、それだけでデータの整合性を保証したり、値のあらゆる用途を保護したりするものではありません。

PHP入力のサニタイズでできること、できないこと

入力の扱いは、項目ごとに求められる条件を確認することから始めます。年齢、メールアドレス、表示名、検索語では、有効とみなされる形式が異なります。検証は値がその条件を満たすかを確かめます。正規化は、前後の空白を取り除くなど、意図が明確で文書化された変換を行います。サニタイザーは文字を削除または変換することがありますが、その結果、ユーザーが送信した内容を意図せず変えてしまう可能性があります。また、変換後の値が後続のあらゆる用途に適するとは限りません。

アプリケーションで元の値を保持する必要がある場合は、正規化またはエンコードした表現と分けて保存します。珍しく見えるという理由だけで文字を削除しないでください。名前などの正当なテキストには、句読点やASCII以外の文字が含まれることがあります。値を使う場面に応じて処理を選ぶことも大切です。HTML出力にはHTMLエンコードを使い、SQLの値にはクエリパラメーターを使います。この2つの保護は互いに置き換えられません。

ユーザー入力を表示するときはHTMLをエスケープする

テキストをHTMLのテキストノードや引用符で囲まれた属性に挿入するときは、エンコードしてください。通常のHTML出力では、適切なフラグを使うと htmlspecialchars() がアンパサンド、不等号、引用符など、HTMLで特別な意味を持つ文字を変換します。htmlentities() は、名前付きHTMLエンティティがある文字もエンコードします。どちらも汎用のHTMLサニタイザーではありません。テキストノード向けに値をエンコードしても、それをJavaScript、CSS、URL、または未加工のHTMLマークアップに挿入して安全になるわけではありません。出力先のコンテキストに合ったエンコーダーとルールを使ってください。

次の自己完結した例では、名前をHTMLのテキストノード向けにエンコードします。コード内のラベルと表示される出力は英語のままです。

<?php
$name = 'Ada & Sam';
echo htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), PHP_EOL;
Ada &amp; Sam

提示された実行時の証拠で、この出力が確認されています。例は Ada &amp; Sam と表示します。これはHTMLのテキストコンテキストで使う文字列であり、SQLやJavaScriptに再利用する値ではありません。

filter_var()で入力を検証し、正規化する

filter_var()は、選択した検証またはフィルタリング処理を適用するもので、HTMLサニタイザーではありません。たとえば、filter_var($email, FILTER_VALIDATE_EMAIL)は、メールアドレスがフィルターに合致すればその値を返し、合致しなければfalseを返します。filter_var($count, FILTER_VALIDATE_INT)では整数を検証でき、特定の範囲が必要な場合はオプションを指定できます。falseは検証失敗を示すため、戻り値を明示的に確認してください。

サニタイズ用フィルターは、入力が有効だと証明するのではなく、入力を変換する場合があります。たとえばFILTER_SANITIZE_EMAILは、そのフィルターで許可されない文字を削除しますが、変換後のアドレスがユーザー本人のものか、メールを受信できるかは確認しません。入力を正規化する場合は、まずルールを定め、その後で結果を検証してください。一般的な解決策としてFILTER_SANITIZE_STRINGに頼るのは避けましょう。マークアップを削除しても、出力先に合ったエンコードにはならず、任意の内容が安全になるわけでもありません。

SQLにはプリペアドステートメントを使う

パラメーター化クエリを使うと、SQLの構文とユーザーが渡した値を分離できます。PDOでは、プレースホルダーを含むクエリを準備し、実行時に値を別途渡します。たとえば、$statement = $pdo->prepare('SELECT id, email FROM Users WHERE lastname = :lastname');の後に$statement->execute(['lastname' => $lastname]);を実行するパターンです。これは使い方を示す断片です。$pdoは対象のデータベースに接続済みである必要があり、データベースサービスとスキーマはこの例の範囲外です。

PDOは任意の入力をサニタイズするものではありません。クエリ内の値についてはパラメーターのバインドがSQLインジェクションを防ぎますが、その値がアプリケーションの要件を満たすかどうかは検証しません。各項目のルールに従って値を検証してください。また、テーブル名や列名などの識別子には別のクエリ構築方法を使います。一般に、識別子を値用のパラメーターとして渡すことはできません。MySQLiもプリペアドステートメントに対応しています。そのインターフェースを使う場合は、パラメーターのバインド用APIを利用してください。

プロフィール項目は型に応じて扱う

プロフィールの各項目には、それぞれ異なるルールを適用します。メールアドレスを検証し、必要に応じてアプリケーションの手順で所有者を確認します。表示名の前後の空白を取り除くのは、それが意図した方針である場合に限ります。郵便番号は、世界共通の形式を仮定せず、該当する地域に照らして検証します。数値項目は、使う前に解析して許容範囲を確認してください。項目の仕様で許される場合は、正当な句読点やUnicode文字を保持します。

保存済みのプロフィール値をページに表示するときは、送信時に検証していても、表示時にHTMLのコンテキストに合わせてエンコードしてください。プロフィールを検索するクエリでは、値をプリペアドステートメントに渡します。検証、正規化、データベースのパラメーター化、出力エンコードを別々の判断として扱うと、処理の意図を把握しやすくなり、1つの変換を万能な保護策と取り違えずに済みます。