全ての記事

なぜ Reactではelement.$$typeofをチェックしてるのか

2018-02-23 @sunderls

react xss

最近iframeでlogicとviewを分離する実験をやってみました。一個問題にあって、それはReactでは全てのelementに$$typeofが追加されることです。興味になってちょっと調べてみました。

jsx

https://reactjs.org/docs/introducing-jsx.html

これはみなさんが知ってると思います。普通jsx でReactを使いますね。

const button = <button className="a-button">click me </button>

babelでコンパイルしたら、だいたいこんな感じのコードになります:

const button = React.createElement('button', {className: 'a-button'}, 'click me');

jsxはブラウザレベルで対応することにはならないです。ECMAScriptの標準にもならなく、たらXML-like syntax extensionです。でもすでに業界な標準でVue.jsなども入れ込んでいます: https://facebook.github.io/jsx/

elementの内部フォマット

ReactではJSONみたいな感じでview treeを管理しています。ちょっとlogしてみましょう。

console.log(<button className="a-button">click me</button>)

これでわかるが、key, props, ref, type以外、$$typeofがついてる!

JSON.stringifyしたら、$$typeofが無くなります。なぜなら、Symbol, undefined, Function などはJSON.stringifyできないからです。


export default class Text extends React.Component {
    state = {
        view: null
    }

    componentDidMount() {
        const view = <button className="a-button">click me </button>;
        console.log(view);
        this.setState({
            view
        });
    }

    render() {
        return <div>{this.state.view}</div>
    }
};

↑のコードはちゃんとrenderできます。

これでstringifyしてからparseしてみたら

componentDidMount() {
    const view = JSON.parse(JSON.stringify(<button className="a-button">click me </button>));
    console.log(view);
    this.setState({
        view
    });
}

やっぱりエラーになりますね。

もし手動で$$typeof追加したら、もんだなくいけます。

componentDidMount() {
    const view = JSON.parse(JSON.stringify(<button className="a-button">click me </button>));
    console.log(view);
    view.$$typeof = Symbol.for('react.element');
    this.setState({
        view
    });
}

なので$$typeofが決め手だ。

$$typeofはなんのため?

名前どおりでは、このelementはReact Elementだよってやつらしいですね。

https://reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html

All React elements require an additional $$typeof: Symbol.for('react.element') field declared on the object for security reasons. It is omitted in the examples above. This blog entry uses inline objects for elements to give you an idea of what’s happening underneath but the code won’t run as is unless you either add $$typeof to the elements, or change the code to use React.createElement() or JSX.

$$typoefはjsxで自動的に追加して、セキュリティー問題を回避できる。

どんなセキュリティー問題?

issueはこちらですね。

Use a Symbol to tag every ReactElement

How Much XSS Vulnerability Protection is React Responsible For?

XSS via a spoofed React element

Improperly validated fields allows injection of arbitrary HTML via spoofed React objects

ちょっと長いので、まとめます

  1. 例えばユーザーが入力できるウェブサイトがあります。入力するフィールドネームはnameとします。
  2. JSがレンダリングする時、APIでnameを取得して、レンダリングします。nameは必ず文字列と想定して、 user: {name: 'some name'} => React.creatElement('div', null, user.name)で描画します。
  3. でもサーバーAPIのバグで、ユーザーからJSON文字列の入力ができ、APIではjson objectになっちゃいます。
    1. 例えば "{"a": "33"}"を入力
    2. APIでは user: {name: {a: '33'}}を返す
    3. React.createElement('div', null, {a: '33'})になる
  4. React.createElementの第三引数では、カスタマイズコンポネントを対応してて、これでユーザーが勝手にcomponentを定義することになります。!!
    1. 例えばこんな文字列"{"type": "div", "_isReactElement": true, "props": {"dangerouslySetInnerHTML": {"__html": "<script>alert('hey')</script>"}}}"
    2. _isReactElement:trueでは自分が有効なcomponentと宣言する
    3. dangerouslySetInnerHTMLで生のhtml断片を挿入する
  5. これでXSS完成。

これはreactの問題ではなく、サーバーの問題では?

このissueの通り、これは確かにサーバーの実装問題、でもReactとして、できる限り対応した方が良いと結論になりました。

実装についてはこのissueでみれる。要するにisValidElementのチェックが甘かった、JSON化できないtypeにする必要。

var data = JSON.parse(decodeURI(location.search.substr(1)));

function Foo(props) {
  return <div><div {...props} /><span>{props.content}</span></div>;
}

ReactDOM.render(<Foo {...data} />, container);

どう解決したの?

symbolは使われました$$typeofは見るようにしました。

var TYPE_SYMBOL = (typeof Symbol === 'function' && Symbol.for &&
    Symbol.for('react.element')) || 0xeac7;

ReactElement.isValidElement = function(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === TYPE_SYMBOL
  );
};

Symbolは文字列出なくて、JSON自ら作れないので、安心。

けどコード見たら、Symbolが存在しないとき定数の0xeac7を使っていますね、もしwindow.Symbolをnullにして、$$typeof0xeac7にしたら、なりすませる?

YES!ただ、reactが動く前にSymbolを上書きする必要がありますね。

<script>
window.Symbol = null;
</script>
<script src="app.js"></script>

これで:

componentDidMount() {
    const view = JSON.parse(JSON.stringify(<button className="a-button">click me </button>));
    console.log(view);
    view.$$typeof = 0xeac7;
    this.setState({
        view
    });
}

うん、通った!

じゃブラウザ自身がwindow.Symbolサポートしてなければ、このセキュリティーホールはまだ存在する?正解!なのでReactを使うときは、絶対Symbol polyfillを入れましょう。

じゃnumber出なくて、function やMath.randomではよくない?よくないです!! ここみて

The fallback solution is a plain well-known number. This makes it unsafe with regard to the XSS issue described in #3473. We could have used a much more convoluted solution to protect against JSON specifically but that would require some kind of significant coordination, or change the check to do a typeof element.$$typeof === 'function' check which would not make it unique to React. It seems cleaner to just use a fixed number since the protection is just a secondary layer anyway. I'm not sure if this is the right tradeoff.

Symbol.for('something')を使ったら、cross-realmになる。つまり、global Symbolになって、iframeやworkerから作ったSymbol.for('something')が一緒!!!

0xeac7?

なぜこの数字? Reactに似てるから w