paper-cut style of tokyo tower

React は javascript スキームを使った XSS を防ぐことができない

React を使用していた場合に引き起こす可能性がある XSS 脆弱性の例として、javascript スキームを使った XSS があります。この記事では、javascript スキームを使った XSS についての説明とその対策について紹介します。

多くのフロントエンドのフレームワークはデフォルトで XSS 対策をしてくれます。例えば、React,Vue.js,Angular といったフレームワークは自動的にエスケープ処理を行ってくれます。

ただし、フレームワークの使用方法を間違えると XSS を引き起こすことがあります。例えば React の dangerouslySetInnerHTML というプロパティを使うと、HTML をエスケープ処理をせずにそのまま埋め込むため、XSS を引き起こすことができます。

以下のコードを実行すると、alert("XSS") が実行されてしまいます。

const html = '<script>alert("XSS")</script>';
 
const App = () => {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
};

このように、フレームワークを使用していても完全に XSS 脆弱性を防ぐことはできません。フロントエンドのセキュリティの正しい知識を身につけた上で、フレームワークの使い方によっては脆弱性を引き起こす可能性がある箇所では、開発者自身によって適切に対処する必要があります。

上記例のように dangerouslySetInnerHTML 使用して HTML をそのまま埋め込む場合には、DOMPurify などのライブラリを使ってサニタイズ行う必要があるでしょう。

import DOMPurify from "dompurify";
 
const html = '<script>alert("XSS")</script>';
const clean = DOMPurify.sanitize(html);
 
const App = () => {
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
};

React を使用していた場合に引き起こす可能性がある XSS 脆弱性の例として、javascript スキームを使った XSS があります。この記事では、javascript スキームを使った XSS についての説明とその対策について紹介します。

javascript スキームを使った XSS とは

javascript スキームを使った XSS とは <a> タグの href 属性に任意のスキームを設定できることを利用した XSS 脆弱性です。

<a> タグの href 属性に指定する URL は HTTP ベースの URL に限定されません。以下に上げるようなブラウザが対応するあらゆるスキームを指定できます。

  • tel::電話番号
  • mailto::メールアドレス
  • file::ローカルファイル
  • javascript::javascript を実行する

javascript: スキームを使うと : 移行の文字列を JavaScript として実行されます。例えば、以下のコードにおいて <a> タグをクリックすると alert("XSS") が実行されます。

<a href="javascript:alert('XSS')">ここをクリック!</a>

ここをクリックをクリックした後に、ブラウザのアラートダイアログが表示されている

javascript スキームを使用した XSS 脆弱性を React でも再現できます。

以下のコードで <a> タグの href 属性に todo.url を渡しているところがポイントです。この値はユーザーが入力した任意の文字列です。この場合、ユーザーが javascript:alert("XSS") javascript スキームを用いた文字列が入力された場合に XSS が発生します。

App.tsx
import React, { useState } from "react";
 
type Todo = {
  id: number;
  title: string;
  url: string;
};
function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [title, setTitle] = useState<string>("");
  const [url, setUrl] = useState<string>("");
  return (
    <div>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          setTodos([...todos, { id: todos.length + 1, title, url }]);
          setTitle("");
          setUrl("");
        }}
      >
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
        />
        <input
          type="text"
          value={url}
          onChange={(e) => setUrl(e.target.value)}
        />
        <button type="submit">Add</button>
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span>{todo.title}</span>
            <a href={todo.url}>{todo.url}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}
 
export default App;

javascript:alert('XSS') という文字列をフォームに入力してサブミットした後に、表示されたリンクをクリックするとブラウザのアラートダイアログが表示されている様子

React 本体でも javascript スキームを使用した XSS 脆弱性が存在することを認識しており、v16.9 からはデベロッパーツールに警告が表示されるようになりました。将来的には、React 本体で対策が行われる可能性があります。

React 本体の v16.9 から追加された javascript スキームを使用した XSS 脆弱性の警告

javascript スキームを使った XSS の対策

それでは、javascript スキームを使用した XSS 脆弱性をどのように対策するのかを見てみましょう。以下の 3 つの方法があります。

  1. http/https スキームのみを許可する
  2. Content Security Policy (CSP) を使用する

http/https スキームのみを許可する

<a> タグの href 属性には http/https スキームの URL しか指定できないようにすることで、javascript スキームを使用した XSS 脆弱性を防ぐことができます。

以下のコードでは、<a> タグの href 属性に todo.url を渡す前に関数で URL をチェックしています。todo.url が http/https スキームの URL であればそのまま渡し、そうでなければ undefined を渡すようにしています。

App.tsx
function App() {
  const checkUrlScheme = (url: string): string | undefined => {
    if (url.match(/^https?:\/\//)) {
      return url;
    } else {
      return undefined;
    }
  };
 
  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span>{todo.title}</span>
            <a href={checkUrlScheme(todo.url)}>{todo.url}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

これにより、javascript: スキームを使用した文字列を入力しても、<a> タグの href 属性には undefined が渡されるため、リンクがクリックされても何も起こりません。

http/https スキームの URL 以外を入力した場合は、通常のリンクとして機能します。

このように、動的な値を <a> タグの href 属性に渡す場合は、必ず値が問題ないかチェックを行う必要があります。

Content Security Policy (CSP) を使用する

Content Security Policy (CSP) は、サーバーからブラウザに対して、どのようなリソースを読み込むことができるかを指定できる仕組みです。CSP を使用することで、XSS や CSS インジェクションなどの脆弱性を軽減できます。

CSP を有効にするためには Content-Security-Policy ヘッダーをレスポンシブに含ませるか、<meta> タグを使用して指定します。

以下の例は Content-Security-Policy ヘッダーを使用しています。

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

CSP では default-srcscript-srcstyle-src のような「ディレクティブ」を用いてどのリソースの読み込みを制限するかを指定します。default-src は、デフォルトでどのようなリソースの読み込みを許可するかを指定します。script-src は、<script> タグで読み込む JavaScript の読み込みを許可するかを指定します。style-src は、<style> タグで読み込む CSS の読み込みを許可するかを指定します。default-src は特に指定のないリソースに対するフォールバックです。

簡単に以下の例を見てみましょう。インラインスタイルを用いて CSS を適用しています。

<p style="color: red">Hello World</p>

赤文字で Hello World と表示されている

ここで style-src ディレクトリを用いて、インラインスタイルの読み込みを許可しないように設定してみましょう。簡易的に <meta> タグを使用して設定してみます。

<head>
  <meta
    http-equiv="Content-Security-Policy"
    content="style-src 'self'"
  />
</head>
<body>
  <p style="color: red">Hello World</p>
</body>

以下のように、インラインスタイルの読み込みが許可されていないため、スタイルが適用されていないことがわかります。デベロッパーツールのコンソールには CSP によりインラインスタイルの読み込みがブロックされたことが表示されます。

黒文字で Hello World と表示されている。デベロッパーツールにはインラインスタイルの読み込みがブロックされたことが表示される

CSP を使用しながらインラインスタイルを使用したい場合は、以下のように unsafe-inline を指定します。しかし、インラインスタイルを許可することは CSP が提供する最大のセキュリティ上の利点を損なうため、どうしても必要な場合を除いては使用しないようにしましょう。

<meta
 http-equiv="Content-Security-Policy"
 content="style-src 'self' 'unsafe-inline'"
/>

セキュリティを維持しながらインラインスタイルを使用したい場合は、nonce を指定する方法があります。nonce は、サーバー側でランダムな文字列を指定することで、その文字列を含むインラインスタイルのみを許可できます。以下の例では、nonce123456 を指定しています。(あくまで例示目的の値であり、実際にはサーバー側でリクエストごとに推測困難なランダムな文字列を生成する必要があります。)

Content-Security-Policy: style-src 'nonce-123456'

以下のように <style>nonce を指定することで使用できます。

<style nonce="123456">
  p {
    color: red;
  }
</style>

CSP について話を進めるとそれだけで 1 つの記事ができてしまうので、このあたりで一旦切り上げることにします。より詳しく知りたい場合には コンテンツセキュリティポリシー (CSP) - HTTP | MDN を参照してください。

javascript スキームを使用した XSS 脆弱性の話に戻りましょう。CSP で script-src ディレクティブを指定するとインラインのスクリプトの実行を防ぐことができます。

先程の React のコードの index.html<meta> タグで CSP を設定してみましょう。

index.html
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />

今度は http/https であるかチェックを実施しないように変更してみます。

App.js
<ul>
  {todos.map((todo) => (
    <li key={todo.id}>
      <span>{todo.title}</span>
      <a href={todo.url}>{todo.url}</a>
    </li>
  ))}
</ul>

狙い通り、スクリプトがブロックされていることがわかりました。デベロッパーツールには CSP によりスクリプトの実行がブロックされたことが表示されます。

javascript:alert('XSS')と書かれたリンクをクリックしても何も発生しない様子

まとめ

  • React では hrefjavascript: スキームを使用すると XSS 脆弱性が発生する
  • 以下の方法で XSS 脆弱性を防ぐことができる
    • href 属性に動的な値を設定する場合は、URL のチェックを行う
    • CSP を使用してインラインスクリプトの実行をブロックする

参考


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事