VueをSSRに乗せると容易にXSSを生み出す場合がある件について

  • 17
    Like
  • 0
    Comment

はじめに
最近Vue.jsを頻繁に使用するのですが、他のSSR(サーバーサイドレンダリング)の仕組みと組み合わせる場合、容易にXSSを生み出してしまうケースが存在するので、注意喚起も兼ねて事例を紹介させていただきます。

前提

  • サーバーサイドで動的に要素をレンダリングするシステムとVue.jsを組み合わせた場合

この記事はrailsのSSRとの組み合わせで解説しますが、プレーンなPHP等、動的にHTMLをレンダリングシステムとの組み合わせでも発生します。

サンプルコード

まず、こちらのコードをご覧ください。

user.erb

<div id="app">
  <div class="user">
    <%= @user.name %>
  </div>
  <button v-on:click="registerFavorite" data-user-id="<%= @user.id %>">お気に入り登録</button>
</div>

user.js

var app = new Vue({
  el: '#app',
  methods: {
    registerFavorite: function (e) { 
      // お気に入り登録のリクエストを送る
    }
  }
})

上記のコードは、サーバーサイドでユーザについてレンダリングした後、お気に入り登録の処理をVue.jsに委譲したコードになります。

erbに含まれる <%= %> で囲まれている部分は、rubyの任意のコードを評価して、戻り値をHTMLエスケープ後、ビューに埋め込む記法です。

今回は、ユーザ名(@user.name)とユーザID(@user.id)をHTMLに埋め込んでいます。

何が危険なのか

Railsの機能によってHTMLエスケープもされるし何が危険なのか」と思うかもしれません。ここで思い出していただきたいのが、Vueの補完機能です。

Vueには補完機能が存在し、mustache記法で任意の場所にJSを埋め込むことが可能です。この記法HTMLではないのでHTMLエスケープされません。

上記のサンプルコードにおいて、@user.name{{ alert(888) }} だった場合、

<div id="app">
  <div class="user">
    {{ alert(888) }}
  </div>
  <button v-on:click="registerFavorite" data-user-id="1">お気に入り登録</button>
</div>

上記のHTMLが出力され、ユーザ入力値がJavascriptとして評価されてしまいます。

便利なフレームワークを使い強固なウェブサイトを構築したはずが、何でもできる便利なサイトに早変わりしてしまいました。

何がつらいのか

「Vue管轄のDOMは絶対にSSRでユーザ入力値を入れてはならない」と言うことを意識して組まなければ簡単に事故り、コストも上がります。

保守面を考えると、上記ルールをテストで担保することは難しいため、新規開発者が加わるたびに周知する必要があり、レビューコストも増大します。

Vue.jsの公式サイトに

他のライブラリや既存のプロジェクトに統合したりすることはとても簡単です。

とありますが、流石にこの状況で既存のプロジェクトへの統合はつらい。。。

対策

補間機能を無効にしましょう!

mustache記法が使えないのは少し不便ですが、脆弱性を生む恐怖やそれが安全であることを保証する工数を考えれば、無効にしたほうが全然良いですよね。

しかし、Vue2.xには補完機能を無効にする機能はありません。
issueも出ていたのでPRを投げましたが反応無し。。。(https://github.com/vuejs/vue/pull/6203)

と言うことで暫定対策を紹介します。

暫定対策

delimiterを指定する機能を使用して補完機能を無効にしましょう。(参考: https://github.com/vuejs/vue/issues/4223)

var app = new Vue({
  // @note 何にもヒットしない正規表現をデリミタにして補間を無効にする。
  delimiters: [ { replace: function() { return '^(?!.).' } }, { replace: function() { return '' } } ],
  el: '#app',
  methods: {
    registerFavorite: function (user_id) { 
      // サーバに指定されたユーザをお気に入り登録のリクエストを送る
    }
  }
})

解説
delimitersに上記の指定をすることで、補間部分を抽出する正規表現が {{((?:.|\\n)+?)}} から ^(?!.).((?:.|\\n)+?) となります。この正規表現は何にもヒットしないのでVueの補完機能を無効化できます。
(参考: https://github.com/vuejs/vue/blob/e259fc306eece6be8014d2ace0258d9775971cd5/src/compiler/parser/text-parser.js#L10)

おわりに

Vueは他のプロジェクトへの統合しやすさを謳っていますが、上記の点に注意しなければ容易にXSSを生み出してしまうことに注意が必要です。

補完機能は、暫定対策で機能を無効にできますが、Vueのバージョンアップ時には暫定対策が有効であるか検証するコストはかかります。

早くPR( https://github.com/vuejs/vue/pull/6203 )マージされてくれ~~~~~ :pray: :pray: :pray: :pray: :pray: :pray: :pray: :pray: