March 5, 2018

最近のJavaScriptフレームワークでのXSS

所謂、最近流行っているJavaScriptフレームワークでのXSSの例をいくつか挙げようと思う。
最近のJavaScriptフレームワークは賢いので、データをバインドする際にHTMLエスケープしてくれてXSSから保護してくれる。
しかしながら、保護が適用されないケースもあるため、過度にフレームワークに信頼しているとXSSを作り込んでしまう。 ここではReactとVue.jsでその例をいくつか挙げようと思う。
あくまで「やってしまうかもしれない例」というだけで、ReactやVue.jsの固有の問題というわけではないです。

React

javascript:スキーム

aタグのhref属性にjavascript:data:text/htmlなどの任意のスキームをユーザーが設定できる場合、XSSが生じる。
これはReactに限った話ではなく、古くからある手法だが、未だにたまに見る。

import React, { Component } from 'react';

class App extends Component {
  render() {
    return (
      <div className="App">
        <a href={this.props.link}>{this.props.link}</a>
      </div>
    );
  }
}

ReactDOM.render(<App link="javascript:alert(1)" />, document.getElementById('root'));

対策としてはスキームをhttp://https://のみに制限するなどする。

ユーザーが任意のpropsを注入できる場合

https://hackerone.com/reports/49652 にあるように、ユーザーの入力値をそのままReact.createElementのpropsとして渡した場合などにはXSSが生じる。

import React from 'react';
import ReactDOM from 'react-dom';

// const props = JSON.parse('{"name": "foo"}');
const props = JSON.parse('{"name": {"dangerouslySetInnerHTML" : { "__html": "<svg/onload=alert(1)>"}}}');
ReactDOM.render(React.createElement("p", props.name), document.getElementById('root'))

dangerouslySetInnerHTML

はい。
https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml

import React, { Component } from 'react';

class App extends Component {
  render() {
    return (
      <div className="App">
        <div dangerouslySetInnerHTML={{__html: '<svg/onload=alert(1)>'}} />
      </div>
    );
  }
}

export default App;

rawHtmlみたいな名前よりdangerouslySetInnerHTMLみたいに危険であることが誰にでも分かるような名前って良いよなーと思います。

Server Side Rendering

React + ReduxでSSRするときにこんなことをすると思う。
https://redux.js.org/recipes/server-rendering#inject-initial-component-html-and-state

const renderFullPage = (html, initialState) => {
    return `
    <!DOCTYPE html>
    <html lang="ja">
      <head>
        <meta charset="utf-8">
        <title>React Router Redux Express</title>
      </head>
      <body>
        <div id="reactbody">
          ${html}
        </div>
        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}
        </script>
        <script src="/js/app.bundle.js"></script>
      </body>
    </html>
    `
};

export default renderFullPage;

脆弱なのは window.__INITIAL_STATE__ = ${JSON.stringify(initialState)} の部分で、initialStateにユーザー入力値が含まれて以下のようになる場合、XSSが生じる。

initialState = {
    name: '"></script><script>alert(1)</script>'
}

Redux公式では ${JSON.stringify(preloadedState).replace(/</g, '\\u003c') のような形で < を置換する形で対応している。
https://redux.js.org/recipes/server-rendering#security-considerations


VueJS

javascript:スキーム

javascript:alert(1)のやつ。

<a v-bind:href="link">link</a>

v-html

はい。
https://jp.vuejs.org/v2/guide/syntax.html#%E7%94%9F%E3%81%AE-HTML

// Safe
<p>Using mustaches: {{ rawHtml }}</p>
// XSS
<p><span v-html="rawHtml"></span></p>

compile

https://vuejs.org/v2/api/#Vue-compile

compileに信用できない値を入れてはいけない。
まぁ、普通こんなことはしないと思うのだけれど…

let res = Vue.compile('<div><img src="x" onerror="alert(1)"></div>')

ユーザーが任意のpropsを注入できる場合

ユーザーが任意のpropsを注入できて、かつ、それをcreateElementに直接渡している場合。

  <div id="app">
    <anchored-heading>Hello world!</anchored-heading>
  </div>

  <script>
  window.addEventListener('load', function () {
    const props = JSON.parse('{"domProps": { "innerHTML": "<img src=\'x\' onerror=\'alert(1)\'>" }}')
    let anchoredHeading = Vue.component('anchored-heading', {
      render: function (createElement) {
        return createElement(
          'h1', props, 'hoge'
        )
      },
    })
    new Vue({
      el: '#app',
      components: {
        'anchored-heading': anchoredHeading
      }
    })
  });
  </script>

Server Side Rendering

RailsとかPHPでちゃんとエスケープされてる…と思ってるとハマる。
http://kamihikouki.hatenablog.com/entry/2017/09/09/175815

ちょっと考えると当たり前ではあるんだけど、コンテキストが異なるのでやってしまう人はいるんじゃないだろうか。

<form action="">
  <label>
    <input
      type="text" name="v" value=""
    />
    <button>Submit</button>
  </label>
</form>

<div id="app">
  <div>
    <?= htmlspecialchars($_GET['v'], ENT_QUOTES, 'utf-8') ?>
  </div>
</div>

<script>
  window.addEventListener('load', function () {
    new Vue({
      el: '#app',
    });
  });
</script>

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.13/dist/vue.js"></script>

このとき$_GET[v]{{ constructor.constructor("alert(1)")() }}など渡すとアラートが出る。
手っ取り早く対策するにはv-preディレクティブを使えば良い。

<div id="app">
  <div v-pre>
    <?= htmlspecialchars($_GET['v'], ENT_QUOTES, 'utf-8') ?>
  </div>
</div>

まとめ

他にも共通なものとしてevalとかあるけど、自明なので省略。
「フレームワークがやってくれてるでしょ」と思って書いてるとうっかり作り込んでしまっていることもあるので、気をつけていきましょう。

このエントリーをはてなブックマークに追加

© Kouhei Morita 2018