JavaScript | Web API:パフォーマンス・セキュリティ - XSS 対策

JavaScript JavaScript
スポンサーリンク

XSS は「ユーザーの入力が“そのままスクリプトになる”事故」

まず、XSS(クロスサイトスクリプティング)のイメージからいきます。

XSS は、ざっくり言うと
「ユーザーが入力した文字列が、そのまま HTML や JavaScript として実行されてしまう」
ことで起きる攻撃です。

掲示板のコメント欄
プロフィールの自己紹介
検索フォームのキーワード表示

こういう「ユーザーが入力したものを画面に出す場所」で、
対策をしていないと簡単に XSS が起きます。

例えば、コメント欄にこんな文字列を投稿されたとします。

<script>alert("あなたのクッキーちょうだい");</script>

それをそのまま HTML に埋め込んでしまうと、
コメントとしてではなく「本物の script タグ」として実行されてしまいます。

これが XSS の一番シンプルな形です。


一番危ないのは「innerHTML にそのまま突っ込む」パターン

悪い例: ユーザー入力を innerHTML に直で入れる

初心者が一番やりがちで、一番危ないのがこれです。

const input = document.querySelector("#nameInput");
const result = document.querySelector("#result");

document.querySelector("#btn").addEventListener("click", () => {
  const name = input.value;
  result.innerHTML = `こんにちは、${name} さん!`;
});
TypeScript

一見、普通のコードに見えますよね。

でも、name にこんな文字列を入れられたらどうなるでしょう。

<script>alert("XSS!");</script>

innerHTML は「HTML として解釈する」ので、
<script> が本物の script タグとして動いてしまいます。

つまり、
「ユーザー入力 → innerHTML」
の直結は、基本的に 即アウト だと思ってください。

対策の基本: 「HTML として解釈させない」

XSS 対策の一番の基本はこれです。

ユーザー入力は「文字」として扱う
決して「HTML」として解釈させない

そのために使うのが textContent です。

const input = document.querySelector("#nameInput");
const result = document.querySelector("#result");

document.querySelector("#btn").addEventListener("click", () => {
  const name = input.value;
  result.textContent = `こんにちは、${name} さん!`;
});
TypeScript

textContent は、
渡された文字列を「そのままテキスト」として表示します。

<script> と書かれていても、
タグとしてではなく「ただの文字」として扱われるので、
スクリプトは実行されません。

ここが、XSS 対策の超重要ポイントです。

innerHTML にユーザー入力を入れない
textContent(または createTextNode)でテキストとして扱う

この 2 行だけでも、かなりの XSS を防げます。


属性・URL に埋め込むときの注意点

属性値にそのまま入れるのも危険になりうる

例えば、ユーザーが入力した URL をリンクにするコード。

悪い例はこうです。

const url = userInput; // ユーザー入力
link.innerHTML = `<a href="${url}">リンク</a>`;
TypeScript

ここでも innerHTML を使っているので危険ですが、
それ以前に「href に何を入れられるか」が問題になります。

例えば、ユーザーがこう入力したらどうでしょう。

javascript:alert("XSS")

href="javascript:..." は、
クリックしたときに JavaScript を実行する仕組みです。
つまり、リンクをクリックした瞬間にスクリプトが走ります。

対策としては、
「許可するスキーム(http, https など)を絞る」ことが重要です。

function safeHref(input) {
  try {
    const url = new URL(input, location.origin);
    if (url.protocol === "http:" || url.protocol === "https:") {
      return url.href;
    }
    return "about:blank";
  } catch {
    return "about:blank";
  }
}

const href = safeHref(userInput);
link.setAttribute("href", href);
link.textContent = "リンク";
JavaScript

ここでは、

URL としてパースできないものは捨てる
http / https 以外のスキームは拒否する

という形で、「危険な URL をそもそも通さない」ようにしています。


テンプレートエンジンやフレームワークに“任せる”のも大事な戦略

手書き innerHTML より「自動エスケープ」に乗る

React / Vue / Svelte などのフレームワークや、
サーバーサイドのテンプレートエンジン(EJS, Pug など)は、
基本的に「デフォルトでエスケープ」してくれます。

例えば、React ならこう書いたとき。

function Hello({ name }) {
  return <p>こんにちは、{name} さん!</p>;
}
JSX

name<script>alert(1)</script> が入っていても、
React が自動的にエスケープしてくれるので、
タグとしてではなく「文字」として表示されます。

逆に、React で危ないのは dangerouslySetInnerHTML のような
「生の HTML を突っ込む」系の API です。

<div dangerouslySetInnerHTML={{ __html: userInput }} />
JSX

これは名前の通り「危険」なので、
ユーザー入力をそのまま入れるのは絶対に避けるべきです。

初心者のうちは、

フレームワークやテンプレートエンジンの「デフォルトのエスケープ」に乗る
どうしても生 HTML を扱うときだけ、慎重に・限定的に使う

というスタンスを持つと、安全側に寄れます。


「入力チェック」と「出力エスケープ」は別物

入力をきれいにしても、それだけでは足りない

よくある誤解に、
「入力時に <> を禁止すれば XSS は防げるでしょ?」
という考え方があります。

確かに、入力チェック(バリデーション)は大事ですが、
それだけでは不十分です。

理由はシンプルで、

入力がどこに出力されるかによって、
危険な文字やパターンが変わるからです。

HTML のテキストとして出すのか
属性値として出すのか
JavaScript の文字列として埋め込むのか

それぞれで、
エスケープすべき文字が違います。

だから、XSS 対策の基本は

入力時に「ありえない形式」を弾く(バリデーション)
出力時に「そのコンテキストに応じたエスケープ」をする

の二段構えです。

特に重要なのは後者、
「出力エスケープ」 です。

テキストとして出すなら HTML エスケープ
属性値なら属性用のエスケープ
URL なら URL エンコード

など、「どこに出すか」を意識して処理を変える必要があります。


Content Security Policy(CSP)で“最後の防波堤”を張る

「もし入り込んでも、実行させない」ための仕組み

XSS 対策の本命は「そもそもスクリプトを埋め込ませない」ことですが、
それでもミスは起こりえます。

そこで使われるのが Content Security Policy(CSP)です。

CSP は、
「このページでは、こういうスクリプトしか実行してはいけない」
というルールをブラウザに伝える仕組みです。

例えば、こんなヘッダをサーバーから返します。

Content-Security-Policy: script-src 'self';

これは、

「このページでは、同じオリジン(自分のサーバー)のスクリプトだけ実行していい」
「インラインの script や、外部の CDN からの script は実行しない」

という意味になります。

これを設定しておくと、
たとえ <script>alert(1)</script> が HTML に紛れ込んでも、
ブラウザが「CSP に反するから実行しない」と止めてくれます。

CSP は少し難しいですが、
覚えておいてほしいのは、

XSS 対策は「エスケープ」だけじゃなくて「実行を制限する」方向もある
CSP はそのための強力な仕組み

ということです。


初心者として「XSS 対策」で絶対に掴んでほしいこと

最後に、要点をギュッとまとめます。

ユーザー入力を innerHTML にそのまま入れない
テキストは textContent(または createTextNode)で扱う
URL や属性に埋め込むときは、許可する形式・スキームを絞る
フレームワークやテンプレートエンジンの「自動エスケープ」に乗る
入力チェックだけで安心しない。「出力エスケープ」が本命
可能なら CSP で「実行できるスクリプト」を制限する

XSS は、
「ちょっとくらい大丈夫でしょ」が一番危ない世界です。

逆に言うと、
今のうちから「innerHTML にユーザー入力を入れない」「必ずテキストとして扱う」
この 2 つを体に染み込ませておけば、
かなりの攻撃を自然に避けられるエンジニアになれます。

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