0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HTMLエスケープの5文字と処理順序 — なぜ「&」を最初に変換しないと二重エスケープになるか

0
Posted at

HTMLエスケープは「特殊文字をエンティティに置換するだけ」の単純処理に見えて、replace を並べる順番を間違えると壊れる。< を先に処理すると二重エスケープが起きる、'&apos; で書いてはいけない、といった地味な罠がある。ぱんだツールズのHTMLエスケープ/アンエスケープは、この5文字の変換と復元をブラウザ内で確認できるツール。

この記事では、HTMLエスケープの実装で踏みやすい「処理順序」と「' の表記」の2つの罠を、実装ベースで書く。

エスケープする5文字

HTMLのテキストとして安全に出すためにエスケープするのは、次の5文字。

文字 エンティティ 理由
& &amp; エンティティの開始文字そのもの
< &lt; タグの開始
> &gt; タグの終了
" &quot; 属性値の囲み(ダブルクォート)
' &#39; 属性値の囲み(シングルクォート)

日本語などASCII範囲外の文字はエスケープしない。あくまでHTMLの構文上意味を持つ記号だけを潰す。実装は素朴な replace の連鎖だが、この5行の順番に意味がある

function escapeHtml(text: string): string {
  return text
    .replace(/&/g, '&amp;')   // ← 必ず最初
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;')
}

罠1:「&」を最初に変換しないと二重エスケープになる

& の置換を最初にやるのが絶対条件。理由は、他の4つのエンティティがすべて & で始まるから。

仮に順番を入れ替えて、< を先に処理したとする。入力 <&lt; になる。ところがこの &lt; には & が含まれている。続けて &&amp; の置換を回すと、さっき自分が作った &lt;& まで巻き込まれて &amp;lt; になってしまう。

入力:  <
悪い順(< を先に):
  .replace(/</, '&lt;')   → &lt;
  .replace(/&/, '&amp;')  → &amp;lt;   ❌ 二重エスケープ

ブラウザはこれを &lt;(文字列)と表示してしまい、< に戻らない。

& を最初に処理すれば、この時点で本文中に生の & しか存在しないので、安全に全部 &amp; へ潰せる。その後に < > " ' を変換しても、新しく生まれる &lt; などの & はもう置換対象として残っていない(&replace は終わっている)。

入力:  <
正しい順(& を先に):
  .replace(/&/, '&amp;')  → <       (& が無いので変化なし)
  .replace(/</, '&lt;')   → &lt;    ✅

A & B <tag> のような混在入力で違いがはっきり出る。& を最初に処理すれば A &amp; B &lt;tag&gt; と正しく変換されるが、順番を崩すと既存エンティティの & を二度打ちして表示が壊れる。「エンティティの開始文字を最初に潰す」——これが順序依存の本質。

アンエスケープは順序が逆で、& を最後に処理する

面白いのは、デコード側(アンエスケープ)ではこの順序がひっくり返ること。実装では &lt; &gt; &quot; &#39; &#x27; &apos; を先に全部戻してから、最後に &amp;& を処理する。

function unescapeHtml(text: string): string {
  return text
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&#x27;/g, "'")
    .replace(/&apos;/g, "'")
    .replace(/&amp;/g, '&')   // ← 必ず最後
}

理由はエスケープと対称で、すでにエスケープ済みの文字列を、もう一段だけエスケープした入力&lt; を再エスケープした &amp;lt; など)を正しく1段階だけ戻すため。もし &amp; を最初に戻すと、&amp;lt; はまず & + lt; = &lt; になり、続く &lt;< の置換でさらに1段階余計にデコードされて < まで戻ってしまう。&amp; を最後に回せば、他のエンティティのパターンマッチが先に完了しているので、&amp;lt;&lt;(1段階だけ戻った状態)で止まる。エスケープは & が先、アンエスケープは & が後——エンティティの開始文字を「新しく作り出す前に潰す」か「他を全部処理し終えるまで残す」かの違いが、逆順という形で現れる。

罠2:シングルクォートは &apos; で書かない

もう1つの罠が '(シングルクォート)の表記。上の実装では '&#39;(数値文字参照) に変換している。&apos; という名前付きエンティティも存在するのに、なぜ使わないのか。

&apos; はXMLとHTML5で定義されたエンティティで、HTML4など古い仕様では未定義。レガシーなHTMLパーサやメールクライアントに &apos; を食わせると、そのまま &apos; という文字列として表示されてしまう環境がある。一方、数値文字参照の &#39;' のコードポイント39を直接指定)は、仕様やパーサの世代に関係なくどこでも ' として解釈される。**「名前付きより数値参照のほうが移植性が高い」**ので、エスケープ出力では &#39; を採用する。

面白いのは、逆方向のアンエスケープでは複数の表記を寛容に受けること。

function unescapeHtml(text: string): string {
  return text
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&#x27;/g, "'")   // 16進の数値参照
    .replace(/&apos;/g, "'")   // 名前付き
}

' に戻せる表記が3つある。&#39;(10進数値参照)、&#x27;(16進数値参照。39 = 0x27)、&apos;(名前付き)。どれも世の中のHTMLで実際に見かけるので、出力するときは最も安全な &#39; に寄せつつ、戻すときは他ツールが吐いた表記も受け付けるという非対称にしてある。エスケープは1通りに正規化、アンエスケープは寛容に、というのは変換系ツールの定石。

このツールでできること・できないこと

この5文字エスケープが守るのは、HTMLのテキストノードや二重引用符で囲んだ属性値というコンテキスト。ユーザー入力をそこに埋め込む前にエスケープすれば、<script> を書かれても &lt;script&gt; になって実行されない。XSSの基本対策だ。

ただし過信は禁物で、コンテキストが変われば必要なエスケープも変わる。href に入れるURL、<script> 内のJavaScript文字列、style 内のCSS——これらは5文字エスケープだけでは守れず、それぞれ専用のエンコードが要る。そして実務では、こうしたエスケープは自分で replace を並べるより、フレームワークの自動エスケープやmarkedとDOMPurifyの組み合わせのようなサニタイザに任せるのが安全。このツールの役割は、テンプレートに渡す前の文字列がどうエスケープされるかを目で確認したり、コピーしてきたHTMLのエンティティを元に戻して読む、という確認用途にある。

なお変換に使っている replace(/&/g, ...) のような正規表現の挙動を試したいときは正規表現テスターで確かめられる。

まとめ

  • HTMLエスケープの5文字は & < > " 'エスケープは & を必ず最初に置換する(他の4エンティティはすべて & で始まるので、順序を崩すと自分が作った &lt;& を二度打ちして二重エスケープ&amp;lt;になる)。アンエスケープは逆に & を最後に処理する(先に戻すと二重デコードされて情報が失われる)
  • シングルクォートは名前付きの &apos;(HTML4で未定義)ではなく、どの世代のパーサでも通る数値文字参照 &#39; で出す
  • アンエスケープは寛容に。'&#39; &#x27; &apos; の3表記すべてを受ける。出力は正規化・入力は寛容、が変換ツールの定石
  • 5文字エスケープが守るのはHTMLテキスト/属性コンテキストまで。URL・JS・CSSは別。実装では自前置換よりサニタイザやフレームワークの自動エスケープに任せる

エスケープ後の見え方の確認や、エンティティの復元にどうぞ。入力はブラウザの外に出ない。

ぱんだツールズ では他にも Markdown→HTML変換・正規表現テスター・URLエンコード・JWTデコーダなど、開発者向けのブラウザ完結ツールを多数公開中。全部無料・登録不要・入力はサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?