XSS対策は「エスケープの順序」で決まる

「エスケープしているのにXSSが起きる」コードには共通点があります。エスケープと整形の順序が壊れているのです。

だめな順序

// 悪い例: 整形してからエスケープ
$html = makeLinks($userText);      // ここで <a href="..."> を生成
$html = htmlspecialchars($html);   // 生成した <a> ごと潰れる…

これでは整形が無意味になるので、実務ではしばしば逆転され、

// もっと悪い例: エスケープしてから…と見せかけて生の値を混ぜる
$html = '<a href="' . $url . '">' . htmlspecialchars($text) . '</a>';

$url が素通りします。javascript:alert(1) を渡せば発火します。

安全な順序

  1. 本文全体を最初にエスケープする
  2. エスケープ済みの文字列に対して整形(リンク化・強調など)を行う
  3. 整形が挿入するタグは、プログラムが組み立てた安全な断片のみ。属性値(URL等)は許可スキームを検査してから入れる

この順序なら「本文由来の文字がタグとして解釈される経路」が存在しなくなります。

URLの検査を忘れない

href/src に入る値は、エスケープとは別にスキーム検査が必要です。

  • 許可: https: http: mailto: および相対パス
  • 拒否: それ以外すべて(javascript: だけを名指しで弾く方式は、&#106;avascript: のような表記揺れに抜かれます)

「何を弾くか」ではなく「何を通すか」で書く。拒否リストではなく許可リスト。

自作のMarkdown変換器でも、この2点(順序と許可リスト)を先に固めてから記法の実装に入りました。順序は後から直すのが最も難しい設計判断だからです。