この記事は、ラクスパートナーズ AdventCalendar 2025の12日目の記事です。
(個人で25日連続投稿にチャレンジ中のカレンダーになります)
今回は、フロントエンド側でサニタイズ(HTMLエスケープ)の処理をほぼバニラ(素のJavaScript)で実装した時の話を書いていきたいと思います。
クロスサイトスクリプティング(XSS)とは?
クロスサイトスクリプティングとは、攻撃者が送り込んだ悪意のコードをそのページを閲覧した不特定多数のユーザーに、スクリプト(簡易的なプログラム)として実行させる可能性があることを指します。「XSS」などと省略されることもあり、このような攻撃をクロスサイトスクリプティング攻撃と言います。
クロスサイトスクリプティング攻撃は、ユーザーからの入力データを表示する仕組みになっている、インターネット掲示板やショッピングサイトのようなWebアプリケーションで特に発生しやすい攻撃です。
引用元
上記のようにWebアプリケーションの脆弱性を突いて悪意のあるコードを実行させる攻撃のことをクロスサイトスクリプティング(XSS) と言います。
対策として、サニタイズを実装します。
サニタイズとは?
サニタイズとは、ユーザーが入力したデータに含まれる特別な意味を持つ可能性がある文字や文字列を、一定の規則に基づき別の表記に置き換えることです。
フロントエンド開発においては、ユーザーがフォームにJavaScriptやHTMLのコードを埋め込んでも、それらを別の文字列に置き換えて無力化することを言います。
DOMPurifyやsanitize-htmlなどのライブラリを用いてサニタイズ処理を実装することが多いようなのですが、私の場合はライブラリを入れずに実装しなければならない事情があったため、ほぼバニラでHTMLエスケープを実装しました。
※ エスケープはサニタイズの一種です。今回はHTML要素をただの文字列として無効化するHTMLエスケープを実装します。
実装例
JavaScriptのreplaceメソッドでHTMLエスケープ実装しました。
(Vue.jsのコードで書いています)
<script setup>
import { ref } from 'vue'
const text = ref('')
const sanitizeText = () => {
text.value = escapeHTML(text.value)
}
const escapeHTML = (str) => {
return String(str)
.replace('&', '&')
.replace('<', '<')
.replace('>', '>')
.replace('"', '"')
.replace("'", ''');
}
</script>
<template>
<div>
<textarea
cols="30"
rows="15"
v-model="text"
@blur="sanitizeText">
</textarea>
<p>{{ text }}</p>
</div>
</template>
上記のサンプルコードでは、& < > " 'の5つの記号を無効化しています。
なぜblurイベントで実行しているのか
「blurイベントではなくてinputイベントで良いのでは?」
と思われるかもしれませんが、
inputイベントでHTMLエスケープ処理を行う場合、Androidなど特定の端末で入力した文字が複数回入力されてしまう(特に全角)
という問題が発生しました。
具体的には
「は」と入力したら「hああああ」
のように、変換がうまくいかなかったり、「あ」が勝手に複数回入力されてしまうのです。
これは、どうやら「IMEによる入力変換中にもinputイベントが発火し続けるため」のようでした。
(以下、ChatGPTの回答のため誤った情報が含まれている可能性があります。ご了承ください)
何が起きるか: Android の多くのキーボードは文字入力を「合成(composition)」として扱います。合成中にアプリ側で v-model の中身を書き換すと、IME の内部状態(未確定のローマ字→仮名変換)と同期が崩れ、文字が二重に挿入されたり、変換が壊れたりします。
なぜ起きるか: IME がまだ「確定」してないテキストを保持しているときに JS 側で value を上書きすると、IMEが保有していた未確定テキストを「確定」処理して再送する実装のキーボードがあり、その結果重複が発生することがあるため。
(同じような問題が起きている方もいらっしゃいました)
https://zenn.dev/luck24247/articles/b5cd9cf6302c97
IMEとは、日本語の入力変換を行ってくれるソフトのことです。こちらがinputイベント中は文字入力の整合性が取れず、合成に失敗するそうです。
しかし、 要素がフォーカスを失った時に発火するblurイベントを試してみたところ、こちらの問題は発生しませんでした(おそらく、入力確定後にHTMLエスケープ処理を行うから、入力変換の整合性も取れているのだと思います)。
以上の理由から、私はblurイベントでHTMLエスケープ処理を実装するようにしています。
おまけ
また、Vue.jsにはv-htmlディレクティブという、文字列の中に含まれているHTML要素を有効にする機能があります。こちらも対策をしないとXSS攻撃を引き起こしてしまうので、どうしても使う場合はエスケープ処理を入れる必要があります。
(そもそもユーザーが入力した値をv-htmlに入れるのは好き放題されてしまうので、避けた方が良さそうですね)
以上となります。
こういったセキュリティ対策はバックエンド側での実装がメインになるかもしれませんが、フロントエンド側でもできる対策はしていきたいですね。
ここまで読んでいただきありがとうございました。