Flatt Security Blog

株式会社Flatt Securityの公式ブログです。プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

株式会社Flatt Securityの公式ブログです。
プロダクト開発やプロダクトセキュリティに関する技術的な知見・トレンドを伝える記事を発信しています。

SPA開発とセキュリティ - DOM based XSSを引き起こすインジェクションのVue, React, Angularにおける解説と対策

f:id:flattsecurity:20220406184000p:plain
Vue.js logo: ©︎ Evan You (CC BY-NC-SA 4.0 with extra conditions(It’s OK to use logo in technical articles for educational purposes)) / React logo: ©︎ Meta Platforms, Inc. (CC BY 4.0) / Angular logo: ©︎ Google (CC BY 4.0)

はじめに

こんにちは。株式会社Flatt Securityセキュリティエンジニアの森(@ei01241)です。

最近のJavaScriptフレームワークの進化は著しく、VueやReactやAngularは様々なWebサービスに採用されています。そのため、多くのWebサービスがSPAを実装するようになりました。JavaScriptフレームワークは便利な一方で、不適切な実装をすると脆弱性を埋め込んでしまいます。どのようなセキュリティ観点が存在するのでしょうか。

本稿では、SPAの脆弱な実装を示し、開発者がSPAを作成する際に気をつけるべき脆弱性とその対策について解説していきます。

※SPAのセキュリティにはインジェクション以外にもセッション管理の問題などがありますが、本稿ではインジェクション脆弱性のみを紹介します。その理由としては、SPAのインジェクションはツールで発見しにくいことがあまり語られていないためです。 また、本稿では全ての脆弱性を網羅する事は目的としておりません。

免責事項

本稿の内容はセキュリティに関する知見を広く共有する目的で執筆されており、脆弱性の悪用などの攻撃行為を推奨するものではありません。許可なくプロダクトに攻撃を加えると犯罪になる可能性があります。当社が記載する情報を参照・模倣して行われた行為に関して当社は一切責任を負いません。

SPAのセキュリティ観点

本章では、SPAで開発者が気をつけるべきセキュリティ観点を説明していきます。

SPAのセキュリティには、CORSやセッション管理の問題からインジェクションまで、様々な脆弱性が存在しますが、代表的なものは大きく以下の2つでしょう。

CORSやセッション管理

SPAのセキュリティでは、まずセッション管理の問題があります。ステートレストークンを使用するかステートフルトークンを使用するかなどの問題や、CORSなどがありますが、以下の記事が詳しいため、こちらの一読を推奨します。

不正なDOM操作によるインジェクション

SPAは内部的にJavaScriptによるDOM操作で見た目を変更しています。そのため、そのDOM操作に不正な入力ができる場合にDOM based XSSが生じることがあります。

これ以降、このようなSPAにおけるインジェクション攻撃に関して紹介していきます。

※各フレームワークでの実装を紹介しますが、フレームワークの良し悪しを判断するわけではありません。

サンプルアプリによるデモ

今回、脆弱性の発現をGIF画像でわかりやすく示すため、サンプルアプリを用意しました。

このアプリには2つの機能があります。 1つ目は、名前を入力したらHTML上に文字列として反映される機能です。

f:id:flattsecurity:20220406184131g:plain:w480

2つ目は、URLを入力したらリンクに飛べる機能です。

f:id:flattsecurity:20220406184433g:plain:w480

さらに、この2機能を持つサンプルアプリをVue, React, Angularの3つのSPAフレームワークを用いて、3パターンそれぞれ用意しました。

詳細は後述しますが、このうちVueとReactを用いたサンプルアプリにはDOM based XSSの脆弱性が存在します。

Burp SuiteによるActive Scan

既存のスキャナツールで脆弱性は検出できるのでしょうか。プロキシツールにBurp Suite Proを使っている場合には、自動脆弱性スキャナとしてActive Scanが使用できます。

紹介したVueとReactのサンプルアプリにActive Scanをかけます。結果は以下の画像のようになります。

f:id:flattsecurity:20220405141316p:plain

何も検出していませんでした。

しかし、後述するようにこのアプリには明確なXSSが存在するにも関わらず、検知できないことがよく起こります。その原因は、脆弱性診断ツールは「診断用の文字列を含むリクエストを投げ、返ってきたレスポンスに対して診断用の文字列の有無を確認する」形式で脆弱性を検出しているからです。

一般的に言って、自動脆弱性スキャナを使った診断では不正なDOM操作による脆弱性の発見が困難です。そのため、SPAにおける不正なDOM操作による脆弱性は手動で見つける他ありません。

また、XSSをはじめとした脆弱性を作り込まないためのセキュアコーディングを実践的なスキルとして身につけるには、Flatt Security が提供する「KENRO」がおすすめです。OS Command Injection や XXE など Web アプリケーションの代表的な脆弱性10個に関して、脆弱なソースコードを修正するなどのハンズオンを通して学ぶことができます。

各SPAフレームワークにおけるXSS

Vue

VueにおけるXSSを紹介します。

v-html

コードは以下のようになっています(必要のないコードは削除しています)。 ユーザの入力文字列をそのままHTMLとして出力しています。

<template>
  <div class="xss">
    <p>Your name is: <span v-html="str1"></span></p>
    <input v-model="str1" placeholder="Input Your Name">
  </div>
</template>

<script>
export default {
  data () {
    return {
        str1: ''
    }
  }
}
</script>

典型的なXSSペイロード<script>alert(1);</script>を入力してみます。

しかし、このペイロードではXSSは発現しません。Vueは内部でinnerHTMLを使用しているため、scriptタグによる実行はできないからです。

では、属性によるJavaScriptはどうでしょうか。

<img src=x onerror='alert(1)'/> を入力してみると、以下のようにアラートが表示されます。

f:id:flattsecurity:20220406185152g:plain:w480

公式ドキュメントに注意書きがあります。

あなたのウェブサイトで任意の HTML を動的にレンダリングすることは、XSS 攻撃 (opens new window)に簡単につながるため、非常に危険です。v-html は信用できるコンテンツのみに使い、ユーザが提供するコンテンツには 決して 使わないでください。

Vueの公式ドキュメントより引用 https://v3.ja.vuejs.org/api/directives.html#v-html

対策としてはv-htmlの代わりにv-textを使用することが挙げられます。 単に入力値を反映するだけの用途であればv-textを使用しましょう。

どうしてもHTMLを使用したい場合にはsanitize-htmlでエスケープすると防げます。

v-textを使用する場合

<template>
  <div class="xss">
    <p>Your name is: <span v-text="str1"></span></p>
    <input v-model="str1" placeholder="Input Your Name">
  </div>
</template>

<script>
export default {
  data () {
    return {
        str1: ''
    }
  }
}
</script>

sanitize-htmlでエスケープする場合

<template>
  <div class="xss">
    <p>Your name is: <span v-html="$sanitize(str1)"></span></p>
    <input v-model="str1" placeholder="Input Your Name">
  </div>
</template>

<script>
export default {
  data () {
    return {
        str1: ''
    }
  }
}
</script>
URLの動的出力

コードは以下のようになっています(必要のないコードは削除しています)。 ユーザの入力文字列をそのままURLとしてhrefに入力します。

<template>
  <div class="xss">
    <p>Your link is: <a v-bind:href="str2">your link</a></p>
    <input v-model="str2" placeholder="Input Your link">
  </div>
</template>

<script>
export default {
  data () {
    return {
        str2: ''
    }
  }
}
</script>

https://flatt.tech などを入力し、your linkをクリックすることで遷移ができますが、ここに javascript:alert(1) を入力すると以下のようにアラートが表示されます。

f:id:flattsecurity:20220406185109g:plain:w480

公式ドキュメントに注意書きがあります。

フロントエンドで URL の無害化 (sanitize)処理を行ったことがある場合、それはすでにセキュリティ上の問題をはらんでいます。ユーザの入力による URL は、常にバックエンドでデータベースに保存する前の処理が必要です。そうすることでモバイルのネイティブアプリを含め、API に接続するすべてのクライアントで問題を回避することができます。また、無害化 (sanitize)処理がされた URL だとしても、Vue はリンク先の安全性を保証することはできません。

Vueの公式ドキュメントより引用 https://jp.vuejs.org/v2/guide/security.html

対策としてはスキームをhttpかhttpsに制限することが挙げられます。 sanitize-urlを使用することもいいでしょう。

<template>
  <div class="xss">
    <p>Your link is: <a v-bind:href="sanitizeUrl(str2)">your link</a></p>
    <input v-model="str2" placeholder="Input Your link">
  </div>
</template>

<script>
export default {
  data () {
    return {
        str2: ''
    }
  }
}
</script>

React

ReactにおけるXSSを紹介します。

dangerouslySetInnerHTML

コードは以下のようになっています(必要のないコードは削除しています)。 ユーザの入力文字列をそのままHTMLとして出力しています。

export default function XSS() {
  
  const [name, setName] = useState('')
  const handleOnChange = (e) => setName(e.target.value)

  return (
    <div className="common">
      <p>Your name is: <span dangerouslySetInnerHTML={{__html: name}}></span></p>
      <input type="text" value={name} onChange={handleOnChange}></input>
    </div>
  );
}

典型的なXSSペイロード<script>alert(1);</script>を入力してみます。

しかし、このペイロードではXSSは発現しません。Reactは内部でinnerHTMLを使用しているため、scriptタグによる実行はできないからです。

では、属性によるJavaScriptはどうでしょうか。

<img src=x onerror='alert(1)'/> を入力してみると、以下のようにアラートが表示されます。

f:id:flattsecurity:20220406194230g:plain:w480

公式ドキュメントに注意書きがあり、非推奨にされています。

dangerouslySetInnerHTML は、ブラウザ DOM における innerHTML の React での代替です。一般に、コードから HTML を設定することは、誤ってあなたのユーザをクロスサイトスクリプティング (XSS) 攻撃に晒してしまいやすいため、危険です。

Reactの公式ドキュメントより引用 https://ja.reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml

対策としてはdangerouslySetinnerHTMLを使用しないことが挙げられますが、どうしてもHTMLを使用したい場合にはsanitize-htmlでエスケープすると防げます。

import sanitizeHtml from 'sanitize-html';

export default function XSS() {
  
  const [name, setName] = useState('')
  const handleOnChange = (e) => setName(e.target.value)

  return (
    <div className="common">
      <p>Your name is: <span dangerouslySetInnerHTML={{__html: sanitizeHtml(name)}}></span></p>
      <input type="text" value={name} onChange={handleOnChange}></input>
    </div>
  );
}
URLの動的出力

コードは以下のようになっています(必要のないコードは削除しています)。 ユーザの入力文字列をそのままURLとしてhrefに入力します。

export default function XSS() {

  const [link, setLink] = useState('')
  const handleOnChangeLink = (e) => setLink(e.target.value)

  return (
    <div className="common">
      <p>Your link is: <a href={link}>your link</a></p>
      <input type="text" value={link} onChange={handleOnChangeLink}></input>
    </div>
  );
}

https://flatt.tech などを入力し、your linkをクリックすることで遷移ができますが、ここに javascript:alert(1) を入力すると以下のようにアラートが表示されます。

f:id:flattsecurity:20220406194258g:plain:w480

対策としてはスキームをhttpかhttpsに制限することが挙げられます。 sanitize-urlを使用することもいいでしょう。

const sanitizeUrl = require("@braintree/sanitize-url").sanitizeUrl;

export default function XSS() {

  const [link, setLink] = useState('')
  const handleOnChangeLink = (e) => setLink(e.target.value)

  return (
    <div className="common">
      <p>Your link is: <a href={sanitizeUrl(link)}>your link</a></p>
      <input type="text" value={link} onChange={handleOnChangeLink}></input>
    </div>
  );
}

Angular

Angularはコンポーネント間をエスケープして渡すため、現状では直接渡してもXSSは発現しませんが、以下に示す例が潜在的に危険な実装であることはVueとReactの例からわかると思います。

innerHTML

コードは以下のようになっています(必要のないコードは削除しています)。 ユーザの入力文字列をそのままHTMLとして出力しています。

<div>
  <p>Your name is: <span [innerHTML]="str1"></span> </p>
  <input type="text" name="str1" [ngModel]="str1" (ngModelChange)="str1=$event"/>
</div>
<router-outlet></router-outlet>

Angularは自動的にHTMLをサニタイズしています。そのため、現状ではXSSは発現しません。

Angular はサニタイズの際に次のいずれかのコンテキストを考慮します。 - HTML HTMLコードとして解釈します。値を innerHtml プロパティにバインドする際などに用いられます。

Angularの公式ドキュメントより引用 https://angular.jp/guide/security

URLの動的出力

コードは以下のようになっています(必要のないコードは削除しています)。 ユーザの入力文字列をそのままURLとしてhrefに入力します。

<div>
  <p>Your link is: <a [href]="str2">your link</a></p>
  <input type="text" name="str2" [ngModel]="str2" (ngModelChange)="str2=$event"/>
</div>
<router-outlet></router-outlet>

Angularは自動的にURLをサニタイズしています。そのため、現状ではXSSは発現しません。

通常、Angularは自動的にURLをサニタイズし、危険なコードを無効にし、 開発モードではこのアクションをコンソールに記録します。

Angularの公式ドキュメントより引用 https://angular.jp/guide/security

おわりに

本稿では、SPAの脆弱な実装によって生じるXSSとその対策について、サンプルアプリを用いて解説しました。

公式ガイドラインをよく読んで、セキュリティ対策をしてください。 本稿を通してSPAを開発する皆様に、安全で安心なサービス構築の一助になれば筆者としては嬉しい限りです。

なお、本稿ではXSSの発現をアラートで表現しましたが、XSSは任意のJavaScriptの実行につながることも多い重大な脆弱性です。そんなXSSの軽視されがちなリスクを具体的に解説する記事を先日公開し、多くの反響をいただきました。

是非、あわせてご覧ください。

Flatt Securityではセキュリティエンジニアの手動診断とツールを組み合わせたセキュリティ診断サービスを提供しています。ツールでは見つけにくいSPAのXSSなどもエンジニアが手動で診断しています。

SPA部分も含めてWebアプリケーション診断を行った事例のインタビューも公開しております。ご興味のある方は、ぜひご覧ください。

flatt.tech

過去に診断を実施したが不安や課題がある、予算やスケジュールに制約がありどのように診断を進めるべきか悩んでいる等、お困り事にあわせて対応策をご提案いたしますので、まずはお気軽にサービスページよりお問い合わせください。

また、Flatt Securityはセキュリティに関する様々な発信を行っています。 最新情報を見逃さないよう、公式Twitterのフォローをぜひお願いします!

twitter.com

ここまでお読みいただきありがとうございました。

参考

Vue

React

Angular