PHP Tips | 文字列処理:基本操作 – 指定タグのみ許可して HTML サニタイズ

PHP PHP
スポンサーリンク

このユーティリティがやりたいことの全体像

「指定タグのみ許可して HTML サニタイズ」というのは、ざっくり言うとこういうことです。

ユーザー入力などの HTML をそのまま表示すると危ないので、
危険なタグや余計なタグは全部消して、
「これだけは OK」と決めたタグだけを残したい。

例えば、

<p>こんにちは、<strong>世界</strong>!</p>
<script>alert('XSS');</script>

という入力が来たときに、

<p>こんにちは、<strong>世界</strong>!</p>

のように、「<p><strong> だけ残して、それ以外は全部落とす」イメージです。

ここで大事なのは、

  • 「全部のタグを消す」のではなく
  • 「一部のタグだけを許可する」

という点です。
これが「指定タグのみ許可して HTML サニタイズ」という考え方です。


まず整理:エスケープとサニタイズの違い

HTML エスケープとは

htmlspecialchars などで、

<script>alert('XSS');</script>
PHP

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

のように、「タグとして動かないただの文字」に変えるのが「エスケープ」です。

これは、

  • HTML として解釈させたくない
  • 画面に「そのままの文字」として見せたい

ときに使います。

HTML サニタイズとは

一方、「サニタイズ」は、

HTML として表示したいけれど、危険なものは取り除きたい

というときに使う考え方です。

  • 太字や改行などの装飾は残したい
  • でも <script>onclick などは消したい

というような場面ですね。

今回のテーマはこの「サニタイズ」、しかも

「指定したタグだけ許可して、それ以外は全部落とす」

というスタイルのサニタイズです。


PHP 標準でできること:strip_tags の「許可タグ」機能

strip_tags で「タグを消す+一部だけ残す」

前の「HTML タグ除去」で出てきた strip_tags には、
「このタグだけは残していいよ」と指定できる機能があります。

基本形はこうです。

$html = '<p>こんにちは、<strong>世界</strong>!<script>alert("XSS");</script></p>';

$sanitized = strip_tags($html, '<p><strong>');

echo $sanitized;
// <p>こんにちは、<strong>世界</strong>!</p>
PHP

ここでやっていることは、

  • <p><strong> は残す
  • それ以外のタグ(ここでは <script>)は全部削除

という動きです。

第2引数に「許可したいタグ」を文字列で渡します。

'<p><strong><em><br>'
PHP

のように、タグをそのまま並べて書きます。
閉じタグ </p> などは書かなくて OK です。

ここまででできること・できないこと

strip_tags の「許可タグ」機能でできるのは、

  • 許可したタグ以外を全部消す
  • 許可したタグは、そのままの形で残す

というところまでです。

逆に言うと、

  • 許可したタグについて「属性(onclick など)までチェックする」ことはできない
  • <a href="javascript:..."> のような「危険な属性」を消すことはできない

という限界があります。

ここが「重要ポイント」なので、もう少し深掘りします。


重要ポイント:タグだけでなく「属性」も危険になりうる

危険なのは <script> だけじゃない

初心者のうちは、

「危ないのは <script> でしょ?」

と思いがちですが、実際にはタグの「属性」も危険になりえます。

例えば、こんな HTML を考えてみましょう。

<a href="javascript:alert('XSS')">クリック</a>

タグ自体は <a> で、一見「リンクだから安全そう」に見えますが、
hrefjavascript: が入っているので、クリックすると JavaScript が実行されます。

strip_tags<a> を許可すると、こうなります。

$html = '<a href="javascript:alert(1)">クリック</a>';

echo strip_tags($html, '<a>');
// <a href="javascript:alert(1)">クリック</a>
PHP

タグは残しつつ「危険な属性だけ消す」ということは、strip_tags だけではできません。

実務でどう考えるか

ここで現実的な選択肢は大きく2つです。

1. 属性を一切許可しない(タグだけ残す)
2. 専用のサニタイズライブラリを使う

初心者向け・シンプルなユーティリティとしては、
まずは「1. 属性を全部落とす」パターンを押さえておくのが現実的です。


シンプルな実装例:指定タグだけ残し、属性は全部削除する

ステップの考え方

やりたいことを分解すると、こうなります。

  1. 許可していないタグは全部削除する
  2. 許可したタグについても、属性は全部削除して「素のタグ」だけにする

例えば、入力がこうだったとします。

<p class="lead">こんにちは、<strong style="color:red;">世界</strong>!</p>
<script>alert("XSS");</script>

これを、

<p>こんにちは、<strong>世界</strong>!</p>

のようにしたい、というイメージです。

実装例:strip_tags + preg_replace で属性を削除

正確な HTML パースは本当は難しいのですが、
「シンプルなケースを対象にした実務ユーティリティ」と割り切るなら、次のような書き方がよく使われます。

function sanitizeHtmlAllowTags(array $allowedTags, string $html): string
{
    // 1. 許可タグを文字列に変換(例: ['p', 'strong'] -> '<p><strong>')
    $allowedTagString = '';
    foreach ($allowedTags as $tag) {
        $allowedTagString .= '<' . $tag . '>';
    }

    // 2. 許可タグ以外のタグを strip_tags で削除
    $html = strip_tags($html, $allowedTagString);

    // 3. 許可タグについても、属性は全部削除する
    //    <tag ...> を <tag> に変換するイメージ
    $html = preg_replace('/<([a-z0-9]+)[^>]*>/i', '<$1>', $html);

    return $html;
}
PHP

使い方はこんな感じです。

$html = '<p class="lead">こんにちは、<strong style="color:red;">世界</strong>!</p>'
      . '<script>alert("XSS");</script>';

$sanitized = sanitizeHtmlAllowTags(['p', 'strong'], $html);

echo $sanitized;
// <p>こんにちは、<strong>世界</strong>!</p>
PHP

この実装のポイント解説

1. 許可タグの指定を配列で受ける

['p', 'strong', 'br']
PHP

のように配列で受けておくと、呼び出し側が分かりやすくなります。

2. strip_tags で「許可タグ以外」を全部削る

$html = strip_tags($html, '<p><strong>');
PHP

の部分で、<script><div> など、許可していないタグは全部消えます。

3. preg_replace で「属性」を全部削る

$html = preg_replace('/<([a-z0-9]+)[^>]*>/i', '<$1>', $html);
PHP

この正規表現は、

  • < に続くタグ名(([a-z0-9]+))をキャプチャして
  • その後ろに続く [^>]*> までの属性部分)を全部無視し
  • <タグ名> だけに置き換える

という動きをします。

つまり、

<p class="lead">

<p> に、

<strong style="color:red;">

<strong> に変換されます。

これで、

  • 許可していないタグは strip_tags で消える
  • 許可したタグも「素のタグ」だけ残る(属性は全部削除)

という状態を作れます。


実務での具体的なシチュエーション例

例題1:お知らせ本文で「最低限の装飾だけ許可」したい

管理画面から「お知らせ本文」を入力できるようにしていて、
担当者には「太字と改行だけ使っていいですよ」と伝えているケースを考えます。

$inputHtml = $_POST['body'] ?? '';

// 許可するタグは <p>, <br>, <strong> のみ
$sanitized = sanitizeHtmlAllowTags(['p', 'br', 'strong'], $inputHtml);

// あとは $sanitized をそのまま HTML として表示
echo $sanitized;
PHP

こうしておけば、

  • <script><iframe> などは全部消える
  • <p>, <br>, <strong> 以外のタグも消える
  • 許可したタグについても、styleonclick などの属性は全部削除される

という状態になります。

「見た目の装飾は最低限でいいから、とにかく安全寄りにしたい」というときの、現実的な落としどころです。

例題2:ユーザーのプロフィール自己紹介欄

ユーザーが自分のプロフィールに「自己紹介文」を書ける機能を考えます。

  • 完全なプレーンテキストだと味気ない
  • でも、あまり自由に HTML を書かせるのも怖い

というときに、

「改行(<br>)とリンク(<a>)だけ許可」

というルールにする、というのはよくあるパターンです。

$inputHtml = $_POST['profile'] ?? '';

// <a> と <br> だけ許可
$sanitized = sanitizeHtmlAllowTags(['a', 'br'], $inputHtml);

echo $sanitized;
PHP

ただし、この場合も href 属性は削除されるので、
本当に「リンクとして機能させたい」なら、もう一段工夫が必要になります(後述)。


もう一歩:リンクだけは href を許可したい場合の考え方

ここから先は少しレベルが上がりますが、実務でよく出る悩みなので触れておきます。

「基本的には属性を全部消したい。
でも <a>href だけは許可したい。」

というケースです。

ここまで来ると、本格的には HTML パーサーや専用ライブラリを使う世界になりますが、
考え方だけ整理しておきます。

考え方のステップ

  1. まず strip_tags で許可タグ以外を削る
  2. DOMDocument などで HTML をパースする
  3. タグごとに「許可する属性」を決めて、それ以外は削除する
  4. href についても、「javascript: を禁止する」などのチェックを入れる

これは初心者向けの「軽いユーティリティ」の範囲を超えるので、
ここでは「属性を全部削るシンプル版」をメインとして押さえておくのが現実的です。


重要ポイントのまとめと実務でのスタンス

重要ポイント

  • 「指定タグのみ許可して HTML サニタイズ」は、
    「全部消す」のではなく「一部だけ残す」発想。
  • PHP 標準の strip_tags で「許可タグ以外を削る」ことは簡単にできる。
  • しかし strip_tags は「属性の安全性」までは見てくれない。
  • シンプルな実務ユーティリティとしては、
    「許可タグ以外を削る」+「許可タグの属性も全部削る」
    という方針が安全寄りで分かりやすい。

実務でのスタンス

  • 「最低限の装飾だけ許可したい」「とにかく安全寄りでいい」
    → 今日紹介したような「タグだけ残して属性は全部削る」ユーティリティがちょうどいい。
  • 「リンクの href もちゃんと使いたい」「もっと細かく制御したい」
    → 専用の HTML サニタイズライブラリや HTML パーサーを検討する段階。

今日から使えるテンプレ関数

最後に、今回の話をギュッと凝縮した「実務でそのまま使える形」をもう一度まとめます。

/**
 * 指定したタグだけを許可し、その他のタグは削除。
 * 許可したタグについても、属性はすべて削除する。
 *
 * 例:
 *   sanitizeHtmlAllowTags(['p', 'strong'], $html);
 */
function sanitizeHtmlAllowTags(array $allowedTags, string $html): string
{
    // 許可タグを strip_tags 用の文字列に変換
    $allowedTagString = '';
    foreach ($allowedTags as $tag) {
        $allowedTagString .= '<' . $tag . '>';
    }

    // 許可タグ以外のタグを削除
    $html = strip_tags($html, $allowedTagString);

    // 許可タグについても属性を削除(<tag ...> -> <tag>)
    $html = preg_replace('/<([a-z0-9]+)[^>]*>/i', '<$1>', $html);

    return $html;
}
PHP

まずはこれを「自分のプロジェクトのどこか一箇所」に組み込んでみると、
HTML 入力まわりの安心感が一段上がります。

タイトルとURLをコピーしました