PHPのエスケープ処理の方法について

採用はこちら

PHPのエスケープ処理とは、文字列をHTMLやURL、SQL、JavaScriptなどで安全に扱えるように処理することです。

ただし、厳密にはすべてを「エスケープ」と呼ぶわけではありません。

HTMLではHTMLエンコード、URLではURLエンコード、JSONではJSONエンコード、SQLではプリペアドステートメントによるパラメータ化など、それぞれ異なる処理を行います。

重要なのは、PHPにはあらゆる用途で使える万能なエスケープ処理は存在しないということです。

データを利用する場所に応じて、適切な方法を使い分ける必要があります。

目次

PHPでエスケープ処理が必要な理由

PHPでWebサイトやWebアプリケーションを開発する場合、フォームやURLパラメータなどからユーザーが入力した値を扱うことがあります。

入力値をそのままHTMLやSQLなどに組み込むと、セキュリティ上の問題が発生する可能性があります。

代表的なものには次のようなものがあります。

  • XSS(クロスサイトスクリプティング)
  • SQLインジェクション
  • HTML構造の崩れ
  • URLの意図しない解釈
  • JavaScriptコードへの不正な値の挿入
  • OSコマンドインジェクション

そのため、ユーザー入力などの外部データを利用するときは、利用する場所に合わせて適切な処理を行うことが重要です。

HTMLではhtmlspecialchars()を使う

PHPで文字列をHTMLへ安全に出力するときは、基本的にhtmlspecialchars()を使用します。

htmlspecialchars()の基本的な使い方

たとえば、次のような文字列がユーザーから送信されたとします。

$name = '<script>alert("XSS")</script>';

この値をそのままHTMLへ出力すると、

echo $name;

scriptタグとしてブラウザに解釈される可能性があります。

そこで、htmlspecialchars()を使用します。

echo htmlspecialchars(
    $name,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

特殊文字がHTMLエンティティへ変換されるため、ブラウザ上では文字列として表示されます。

たとえば、

&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;

のような形式になります。

ENT_QUOTESを指定する理由

ENT_QUOTESを指定すると、ダブルクォートとシングルクォートの両方を変換できます。

特にHTML属性へユーザー入力を出力するときに重要です。

<input
    type="text"
    value="<?= htmlspecialchars(
        $value,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    ) ?>"
>

属性値を引用符で囲み、適切にエスケープすることで、ユーザー入力によってHTML構造が壊れるリスクを抑えられます。

ENT_SUBSTITUTEを指定する理由

ENT_SUBSTITUTEを指定すると、不正な文字コードシーケンスが含まれていた場合に、代替文字へ置き換えられます。

そのため、実務では次のような形で使用すると分かりやすいでしょう。

htmlspecialchars(
    $value,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

HTMLエスケープ用の関数を作る方法

Webアプリケーションでは、htmlspecialchars()を何度も記述することがあります。

そのため、独自のヘルパー関数を用意する方法もあります。

function h(?string $value): string
{
    return htmlspecialchars(
        $value ?? '',
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );
}

すると、HTMLでは次のように記述できます。

<p><?= h($name) ?></p>

ただし、h()はPHP標準の関数ではありません。

プロジェクト側で独自に定義した関数です。

htmlspecialchars()とhtmlentities()の違い

PHPには、htmlspecialchars()に似たhtmlentities()もあります。

htmlspecialchars()の特徴

htmlspecialchars()は、HTML上で特別な意味を持つ主な文字を変換します。

代表的な対象は次のとおりです。

&
"
'
<
>

通常のHTML出力でXSS対策を行う場合は、基本的にhtmlspecialchars()で十分です。

htmlentities()の特徴

htmlentities()は、htmlspecialchars()より広い範囲の文字をHTMLエンティティへ変換します。

一般的なWebサイトでユーザー入力を表示する目的であれば、通常はhtmlspecialchars()を使用するケースが多いでしょう。

フォームの入力値を安全に表示する方法

フォームで入力された内容を再表示するときも、HTMLエスケープが必要です。

たとえば、次のように値を取得したとします。

$name = $_POST['name'] ?? '';

そのまま、

<input type="text" value="<?= $name ?>">

と出力するのは避けたほうがよいでしょう。

次のようにエスケープします。

<input
    type="text"
    value="<?= htmlspecialchars(
        $name,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    ) ?>"
>

textareaでもエスケープする

textareaに表示する場合も同様です。

<textarea><?= htmlspecialchars(
    $message,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
) ?></textarea>

ユーザーが入力した内容だから危険なのではなく、HTMLとして解釈される場所へ外部データを出力するため、適切な処理が必要になると考えることが重要です。

HTML属性では出力先にも注意する

htmlspecialchars()を使えば、すべてのHTML属性が安全になるわけではありません。

通常のテキスト属性

たとえば、

value
title
alt

などの通常の文字列属性では、属性値を引用符で囲んだうえでhtmlspecialchars()を使用する方法が基本です。

URLを含む属性

一方、

href
src

などはURLとしても解釈されます。

たとえば、

<a href="<?= htmlspecialchars(
    $url,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
) ?>">

とした場合、HTML構文としてのエスケープは行われます。

しかし、URL自体が適切かどうかまではhtmlspecialchars()では判断されません。

ユーザーがURLを指定できる場合は、URLとしての妥当性や許可するスキームなども別途確認する必要があります。

つまり、htmlspecialchars()は値そのものを無条件に安全にする関数ではなく、HTMLとして安全に出力するための関数です。

SQLではhtmlspecialchars()を使わない

SQLインジェクション対策としてhtmlspecialchars()を使用するのは誤りです。

次のような処理は適切ではありません。

$name = htmlspecialchars(
    $_POST['name'],
    ENT_QUOTES,
    'UTF-8'
);

$sql = "SELECT * FROM users WHERE name = '$name'";

htmlspecialchars()はHTML用の関数であり、SQLの構文を安全にするものではありません。

SQLでは、プリペアドステートメントとプレースホルダーを使用する方法が基本です。

PDOでSQLインジェクションを防ぐ方法

PDOでは、SQL文と値を分離して処理できます。

SELECT文の例

$name = $_POST['name'] ?? '';

$stmt = $pdo->prepare(
    'SELECT * FROM users WHERE name = :name'
);

$stmt->execute([
    ':name' => $name,
]);

$user = $stmt->fetch();

:nameがプレースホルダーです。

ユーザー入力をSQL文字列へ直接連結せず、値として渡すことが重要です。

INSERT文の例

$stmt = $pdo->prepare(
    'INSERT INTO users (name, email)
     VALUES (:name, :email)'
);

$stmt->execute([
    ':name' => $name,
    ':email' => $email,
]);

SQLでは、文字列を自分でエスケープすることよりも、SQL文と値を分離する考え方が重要です。

mysqliでもプリペアドステートメントを使える

mysqliを使用している場合も、プリペアドステートメントを利用できます。

$stmt = $mysqli->prepare(
    'SELECT * FROM users WHERE email = ?'
);

$stmt->bind_param('s', $email);
$stmt->execute();

次のようにユーザー入力を直接連結する書き方は避けます。

$sql = "
    SELECT *
    FROM users
    WHERE email = '" . $_POST['email'] . "'
";

SQLのプレースホルダーには使えない場所もある

プリペアドステートメントを使用すれば、SQLのあらゆる部分を自由に置き換えられるわけではありません。

プレースホルダーは基本的に値へ使う

次のような使い方は問題ありません。

WHERE id = :id

しかし、次のような書き方はできません。

SELECT * FROM :table

プレースホルダーは基本的にデータ値を渡すためのものです。

そのため、次のようなSQLの構成要素は別の方法で管理する必要があります。

  • テーブル名
  • カラム名
  • ASC
  • DESC
  • SQLキーワード

カラム名などは許可リストで制御する

たとえば、並び替えるカラムをユーザーが指定できる場合は、許可する値をあらかじめ決めます。

$allowedColumns = [
    'name',
    'created_at',
    'price',
];

$sort = $_GET['sort'] ?? 'created_at';

if (!in_array($sort, $allowedColumns, true)) {
    $sort = 'created_at';
}

このような許可リスト方式を使うことで、想定外のSQL構文が組み込まれるのを防ぎやすくなります。

prepare()を呼ぶだけでは安全にならない

プリペアドステートメントは重要ですが、prepare()を使用していれば無条件で安全になるわけではありません。

たとえば、次のコードには注意が必要です。

$sql = "SELECT * FROM users ORDER BY " . $_GET['sort'];

$stmt = $pdo->prepare($sql);
$stmt->execute();

$_GET['sort']はすでにSQL文字列へ直接連結されています。

この部分はプレースホルダーで保護されていません。

SQLインジェクション対策では、ユーザー入力をSQL文字列へ直接組み込まないことが重要です。

URLではrawurlencode()などを使う

URLへ文字列を組み込む場合は、HTMLとは異なる処理が必要です。

URLの値をエンコードする

たとえば、

$keyword = 'PHP 入門';

$url = '/search?q=' . rawurlencode($keyword);

とすると、URLとして扱いやすい形式へ変換できます。

rawurlencode()をURL全体へ使わない

rawurlencode()は、通常URL全体へ一括で使用するものではありません。

たとえば、

rawurlencode(
    'https://example.com/search?q=PHP'
);

とすると、URL構造を構成する:/までエンコードされます。

そのため、URLパスの一部分やパラメータ値など、必要な箇所へ使用します。

クエリ文字列にはhttp_build_query()が便利

複数のGETパラメータをURLへ付与するときは、http_build_query()が便利です。

$params = [
    'keyword' => 'PHP 入門',
    'page' => 2,
];

$query = http_build_query($params);

$url = '/search?' . $query;

複数のパラメータを手動で連結するよりも、コードを分かりやすくできます。

JSONではjson_encode()を使う

PHPの値をJSON形式へ変換するときは、json_encode()を使用します。

$data = [
    'name' => 'Taro',
    'age' => 30,
];

$json = json_encode($data);

JavaScriptへPHPの値を渡す場合にも利用できます。

JavaScriptへ値を渡す例

<script>
const name = <?= json_encode(
    $name,
    JSON_HEX_TAG
    | JSON_HEX_AMP
    | JSON_HEX_APOS
    | JSON_HEX_QUOT
) ?>;
</script>

必要に応じて、

JSON_THROW_ON_ERROR

を追加すると、JSONエンコードに失敗した場合に例外として検出できます。

<script>
const name = <?= json_encode(
    $name,
    JSON_HEX_TAG
    | JSON_HEX_AMP
    | JSON_HEX_APOS
    | JSON_HEX_QUOT
    | JSON_THROW_ON_ERROR
) ?>;
</script>

JavaScriptではaddslashes()に頼らない

PHPにはaddslashes()という関数があります。

$text = addslashes($text);

addslashes()は、次のような文字の前にバックスラッシュを付けます。

  • シングルクォート
  • ダブルクォート
  • バックスラッシュ
  • NULLバイト

しかし、addslashes()をWebセキュリティ全般のためのエスケープ関数として使用するのは適切ではありません。

HTMLならhtmlspecialchars()、SQLならプリペアドステートメント、JSONならjson_encode()のように、それぞれ専用の処理を使用します。

シェルコマンドではescapeshellarg()などを使う

PHPから外部コマンドを実行する場合は、HTMLやSQLとは別の対策が必要です。

escapeshellarg()の使い方

シェルへ1つの引数として値を渡したい場合は、escapeshellarg()を使用できます。

$filename = 'example file.txt';

$command = 'cat ' . escapeshellarg($filename);

ユーザー入力が複数のシェル引数として解釈されることを防ぎやすくなります。

escapeshellcmd()だけに頼らない

PHPにはescapeshellcmd()もあります。

ただし、escapeshellcmd()を使用すれば、任意のユーザー入力を安全にコマンドとして実行できるという意味ではありません。

可能であれば外部コマンドの実行自体を避け、必要な場合でも、

  • 実行するコマンドを固定する
  • ユーザー入力を個別の引数として扱う
  • 入力値を許可リストで制限する

などの対策を併用することが重要です。

エスケープは保存時ではなく出力時が基本

PHPで扱うデータを、受け取った時点ですべてHTMLエスケープして保存する方法は、一般的にはおすすめできません。

たとえば、

$name = htmlspecialchars(
    $_POST['name'],
    ENT_QUOTES,
    'UTF-8'
);

としてからデータベースへ保存すると、HTML出力用に変換されたデータが保存される可能性があります。

しかし、そのデータは後から、

  • HTML
  • JSON
  • CSV
  • メール
  • PDF
  • API

などで利用するかもしれません。

そのため、基本的には元の意味を保持したデータを保存し、利用する場所に応じて処理します。

入力
↓
バリデーション
↓
必要に応じた正規化
↓
保存
↓
利用する場所に応じた処理

HTMLならHTMLエンコード、SQLならパラメータバインド、URLならURLエンコードというように使い分けます。

バリデーションとエスケープは別物

PHPでは、バリデーションとエスケープを混同しないことも重要です。

バリデーションとは

バリデーションは、入力された値が想定した条件を満たしているかを確認する処理です。

たとえば年齢を整数として確認する場合は、

$age = filter_input(
    INPUT_POST,
    'age',
    FILTER_VALIDATE_INT
);

のようにできます。

メールアドレスであれば、

$email = filter_var(
    $email,
    FILTER_VALIDATE_EMAIL
);

のような検証もできます。

エスケープとは

一方、エスケープやエンコードは、データを利用する場所に応じて安全な形式へ変換する処理です。

たとえば、検証済みのメールアドレスでもHTMLへ表示するときには、

echo htmlspecialchars(
    $email,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

のような処理が必要です。

つまり、

バリデーション
→ 入力値が正しい形式か確認する

エスケープ・エンコード
→ 出力先で安全に扱える形式にする

という違いがあります。

二重エスケープに注意する

同じ文字列へ何度もhtmlspecialchars()を適用すると、二重エスケープが発生することがあります。

たとえば、

&

が、

&amp;

へ変換され、さらにエスケープすると、

&amp;amp;

となる可能性があります。

htmlspecialchars()には第4引数として$double_encodeがありますが、基本的にはこれだけに頼るよりも、

元データを保持し、HTMLへ出力するときに一度だけエスケープする

という設計に統一するほうが管理しやすくなります。

nl2br()と組み合わせる場合

ユーザーが入力した改行をHTML上でも表示したい場合は、htmlspecialchars()を適用してからnl2br()を使用します。

echo nl2br(
    htmlspecialchars(
        $message,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    )
);

順番が重要です。

先にnl2br()を実行してからHTMLエスケープすると、nl2br()が生成した<br>まで文字列としてエスケープされてしまいます。

PHPのエスケープ処理を用途別に整理

PHPでよく使われる処理をまとめると、次のようになります。

使用する場所主な方法
HTML本文htmlspecialchars()
通常のHTML属性値引用符+htmlspecialchars()
URLパスの一部分rawurlencode()
URLクエリhttp_build_query()
SQLの値プリペアドステートメント+パラメータバインド
SQLのテーブル名・カラム名許可リストで制御
JSONjson_encode()
JavaScriptへのデータ受け渡しjson_encode()など
シェル引数escapeshellarg()
シェルコマンド動的生成を避け、必要なら厳格に制限

PHPのエスケープ処理でよくある間違い

すべてhtmlspecialchars()で処理する

htmlspecialchars()はHTML用です。

SQLやURL、シェルコマンドへ使用しても、それぞれの脆弱性を適切に防げるわけではありません。

SQLを文字列連結で作る

次のようなコードは避けます。

$sql = "
    SELECT *
    FROM users
    WHERE id = " . $_GET['id'];

SQLの値にはプリペアドステートメントを使用します。

入力時にHTMLエスケープして保存する

データベースへHTML向けに加工した値を保存すると、JSONやCSVなど別の用途で利用しにくくなることがあります。

基本的には元データを保持し、出力するときに適切な処理を行います。

エスケープだけですべて安全になると考える

Webアプリケーションのセキュリティ対策は、エスケープだけでは完結しません。

必要に応じて、

  • 入力値のバリデーション
  • 認証
  • 認可
  • CSRF対策
  • セッション管理
  • Content Security Policy
  • 適切なHTTPヘッダー

なども組み合わせる必要があります。

PHPのエスケープ処理で重要な4つのポイント

1.万能なエスケープ関数は存在しない

HTML、SQL、URL、JSON、JavaScript、シェルでは必要な処理が異なります。

利用する場所に応じて方法を選びましょう。

2.HTMLではhtmlspecialchars()を基本にする

通常のHTML本文や属性値へ文字列を出力するときは、

htmlspecialchars(
    $value,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

を基本にします。

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

SQLではユーザー入力を文字列連結せず、

$stmt = $pdo->prepare(
    'SELECT * FROM users WHERE id = :id'
);

$stmt->execute([
    ':id' => $id,
]);

のように値をパラメータとして渡します。

テーブル名やカラム名などはプレースホルダー化できないため、必要に応じて許可リストで制御します。

4.出力先に応じて処理する

データを保存するときに一律で加工するのではなく、利用する場所に応じて適切な処理を行うことが重要です。

HTML
→ htmlspecialchars()

SQL
→ プリペアドステートメント

URL
→ rawurlencode()・http_build_query()

JSON
→ json_encode()

シェル
→ escapeshellarg()など

PHPのエスケープ処理のまとめ

PHPのエスケープ処理で最も重要なのは、「何をエスケープするか」ではなく「どこで使用するデータなのか」を考えることです。

HTMLへ出力する場合はhtmlspecialchars()を使用し、SQLではプリペアドステートメントとパラメータバインドを利用します。

URLではrawurlencode()http_build_query()、JSONではjson_encode()、シェル引数ではescapeshellarg()など、用途に応じた方法を選ぶことが重要です。

また、バリデーションとエスケープは役割が異なります。

入力値が適切かどうかを確認したうえで、実際にHTMLやSQLなどで利用するタイミングに適した処理を行いましょう。

PHPで安全なWebアプリケーションを開発するためには、「データを一律にエスケープする」のではなく、「利用する文脈ごとに正しい方法を選ぶ」ことが基本です。

以上、PHPのエスケープ処理の方法についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次