CODE Polyglot / よく使う書き方 / 02

HTML の特殊文字をエスケープする

利用者が入力した文字を HTML に埋め込む前に、< > & " ' を &lt; などに置き換えます。標準の関数は言語ごとに ' の書き方が違い、この決まりの形とそのまま一致したのは Ruby だけでした。

画面にこう出れば正解

どの言語で書いても、同じ入力なら画面に出る文字は1文字も違わない、というのが正解の決め方です(例は Python で動かしたときのコマンドです)。

タグ

$ python3 escape.py "<b>太字</b>"
&lt;b&gt;太字&lt;/b&gt;

5つすべて

$ python3 escape.py "& < > \" '"
&amp; &lt; &gt; &quot; &#39;

属性に入れる値(' と ")

$ python3 escape.py "It's \"OK\""
It&#39;s &quot;OK&quot;

すでにエスケープしてある

$ python3 escape.py "&amp; &lt;"
&amp;amp; &amp;lt;

スクリプト

$ python3 escape.py "<script>alert('x')</script>"
&lt;script&gt;alert(&#39;x&#39;)&lt;/script&gt;

変える文字が無い

$ python3 escape.py "こんにちは 🍣"
こんにちは 🍣

空の文字列

$ python3 escape.py ""

改行はそのまま

$ python3 escape.py "1行目
<2行目>"
1行目
&lt;2行目&gt;

10言語のプログラム

どれもそのまま保存して動かせます。毎朝、上の正解の例をすべて実際に動かして確かめています。

escape.php8.4.26 で 8 / 8 件の出力が正解と一致

<?php
// 受け取った文字列の & < > " ' を、HTML の書き方(&amp; &lt; &gt; &quot; &#39;)に変える
$s = $argv[1];

// htmlspecialchars は ' を &#039; と書く(この回の &#39; と違う)ので、自分で置き換える。
// strtr に配列を渡すと、元の文字列を1回だけ見て置き換えるので、作った &lt; の & がもう一度変わることがない
echo strtr($s, [
    '&' => '&amp;',
    '<' => '&lt;',
    '>' => '&gt;',
    '"' => '&quot;',
    "'" => '&#39;',
]) . "\n";

動かし方(ファイルを保存したフォルダで)

$ php escape.php "<b>太字</b>"

よく見かける書き方で試すと

仕事でよく見かける書き方(標準の関数をそのまま使う形や、よく紹介される正規表現など)を、同じ正解表で動かした結果です。言語を出した答えごとにまとめ、赤い字はその言語が実際に出した間違った行です。

入力よく見かける書き方の答え(7言語)
タグ"<b>太字</b>"
  • JavaScript&lt;b&gt;太字</b>
  • Java・Kotlin&amp;lt;b&amp;gt;太字&amp;lt;/b&amp;gt;
  • PHP・Python・Go・C#✓ 正解
5つすべて"& < > \" '"
  • PHP&amp; &lt; &gt; &quot; &#039;
  • Python&amp; &lt; &gt; &quot; &#x27;
  • Go&amp; &lt; &gt; &#34; &#39;
  • Java・Kotlin&amp; &amp;lt; &amp;gt; &amp;quot; &amp;#39;
  • JavaScript・C#✓ 正解
属性に入れる値(' と ")"It's \"OK\""
  • PHPIt&#039;s &quot;OK&quot;
  • PythonIt&#x27;s &quot;OK&quot;
  • GoIt&#39;s &#34;OK&#34;
  • JavaScriptIt&#39;s &quot;OK"
  • Java・KotlinIt&amp;#39;s &amp;quot;OK&amp;quot;
  • C#✓ 正解
すでにエスケープしてある"&amp; &lt;"
  • JavaScript&amp;amp; &lt;
  • PHP・Python・Go・Java・C#・Kotlin✓ 正解
スクリプト"<script>alert('x')</script>"
  • PHP&lt;script&gt;alert(&#039;x&#039;)&lt;/script&gt;
  • Python&lt;script&gt;alert(&#x27;x&#x27;)&lt;/script&gt;
  • JavaScript&lt;script&gt;alert(&#39;x')</script>
  • Java・Kotlin&amp;lt;script&amp;gt;alert(&amp;#39;x&amp;#39;)&amp;lt;/script&amp;gt;
  • Go・C#✓ 正解
変える文字が無い"こんにちは 🍣"
  • C#こんにちは &#127843;
  • PHP・Python・Go・JavaScript・Java・Kotlin✓ 正解
空の文字列""

✓7言語すべて正解

改行はそのまま"1行目 <2行目>"
  • Java・Kotlin&amp;lt;2行目&amp;gt;
  • PHP・Python・Go・JavaScript・C#✓ 正解

この書き方の決まり

  • & < > " ' の5つを、&amp; &lt; &gt; &quot; &#39; に変える。それ以外の文字(日本語・絵文字・改行)は変えない
  • & を最初に変える
  • すでに &amp; と書いてあっても、もう一度変える(&amp;amp;)

解説

' の書き方が、言語ごとに違う

' を &#39;・&#039;・&#x27; のどれで書いても、ブラウザでは同じ ' として表示されます。ただ、出力を文字列として比べると違うものになります。テストの期待値や、キャッシュのキーに使うときに困ります。

言語の標準の関数'"そのほか
PHP htmlspecialchars&#039;&quot;PHP 8.1 から、何も指定しなくても ' を変える
Python html.escape&#x27;&quot;
Go html.EscapeString&#39;&#34;
C# WebUtility.HtmlEncode&#39;&quot;絵文字も &#127843; のような数字の形に変える
Ruby CGI.escapeHTML&#39;&quot;この決まりとそのまま一致
JavaScript・Java・Kotlin・Rust・Swift標準に関数が無い

この書き方では、Ruby だけ標準の関数を使い、ほかは自分で置き換える形にしました。PHP の strtr、Python の translate、Go の strings.NewReplacer は、元の文字列を1回だけ見て置き換えるので、変える順番を気にしなくて済みます。

置き換えをつなぐときは、& を最初に

replace を5回つなぐ書き方では、& を最後に変えると、先に作った &lt; の & まで変わって &amp;lt; になります(Java・Kotlin の naive)。つなぐなら & を最初にします。

JavaScript の replace は、最初の1つしか変えない

JavaScript の s.replace("<", "&lt;") は、最初に見つかった1つだけを変えます。Java の replace や C# の Replace はすべて変えるので、同じ名前でも動きが違います。すべて変えるには replaceAll を使うか、/</g のように g を付けた正規表現を渡します。

二重にエスケープしない

この決まりでは、&amp; をもう一度変えて &amp;amp; にします。テンプレートエンジン(多くは自動でエスケープする)に渡す値を自分でもエスケープすると、画面に &amp; と出ます。エスケープは「HTML に埋め込む直前に1回だけ」が原則です。