Slide 1

Slide 1 text

React Hooksに潜む罠 Security.Tokyo #1 icchy

Slide 2

Slide 2 text

whoami ● セキュリティエンジニアをやっている ● 社内で使うツールを書く必要があり、なんとなくReactを始めた ○ 2年前くらい ○ 普段から書いているわけではない ○ つまり初心者 ● **フロントエンドの専門家ではありません** ○ 発表内容には間違いがある可能性があります ○ ソースコードもそこまで深く読んではいません ● CTFをやっていた(過去)

Slide 3

Slide 3 text

今日お話しする内容について ● React Hooksの既知の挙動 ○ 0-dayではありません ● CTFの問題として取り上げられた ○ picoCTF 2022 "live-art" by Zachary Wade (@zwad3) ○ 脆弱性のある実装パターンを問題にしたのはおそらくこれが初出 ○ https://github.com/zwade/live-art/blob/master/solution/writeup.md ● 詳しく調べてみたところ、かなり面白いバグだったので紹介 ○ (おそらく)一般的なプロダクトでもやってしまいそうな気がする ● 発表者がReactにあまり明るくありません ○ 変な間違いがあったら後でこっそり教えてください ○ 憶測で書いている箇所は斜体かつオレンジにしてあります

Slide 4

Slide 4 text

Reactとは?

Slide 5

Slide 5 text

React ● "ユーザインターフェース構築のための JavaScript ライブラリ" ● Jordan Walke (元Facebook) が開発、2013年にOSS化 ○ 以降はFacebook (Meta) やコミュニティによって継続的に開発 ● JSX (TSX) で書かれた宣言的UI ○ 変なDOM操作をしなくてよい ● XSSが発生しにくい ● 状態管理をするための機能が用意されている ○ React 16.8 からReact Hooksと呼ばれるシンプルなものが導入された ○ クラスではなく関数として使用可能

Slide 6

Slide 6 text

命令的UIと宣言的UI ● 命令的 (Imperative) ○ 目的に向けた過程を記述 ○ DOMを指定して、その中に値をセット ○ 仕様を知らないと完成形がイメージできない ● 宣言的 (Declarative) ○ 最終的にどうしたいかを記述 ○ テンプレートを用意する ○ 完成形が比較的イメージしやすい ● UIを作る目的においては宣言的の方が都合が良い

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Reactって何のためにあるの? ● 一般にブラウザがDOMを描画するコストは高い ○ 不必要な計算はなるべくしないことが大事 ○ UIに限らずあらゆるアルゴリズムで言えること ● 差分検出処理 (Reconcilation) をよしなにやる ○ 状態遷移の前後で異なる DOMだけを計算する ■ 必要最低限のDOMを再描画する ○ Reactに限らずVue.jsでも同じようなことをやっている (patch) ● なので(レンダリングが)早い、(メモリも)軽い https://ja.reactjs.org/docs/rendering-elements.html

Slide 9

Slide 9 text

Reactって何やってるの? ● 差分検出をめちゃくちゃ賢くやっている ○ https://ja.reactjs.org/docs/reconciliation.html ● React 16からReact Fiberと呼ばれるエンジンに変わった ○ https://github.com/acdlite/react-fiber-architecture ● 状態管理やメモ化のための機能を提供している → React Hooks

Slide 10

Slide 10 text

React Hooks

Slide 11

Slide 11 text

React Hooks ● コンポーネントが内部で持つ状態を管理することができる ● プログラマは「状態遷移の処理」を書く ○ 処理の結果、何を描画すべきということについては Reactが管理 clicked: 1 button clicked: 2 button count = count + 1 count: 1 count: 2

Slide 12

Slide 12 text

React Hooks import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return (

You clicked {count} times

setCount(count + 1)}> Click me
); }

Slide 13

Slide 13 text

React Hooks import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return (

You clicked {count} times

setCount(count + 1)}> Click me
); }

Slide 14

Slide 14 text

React Hooks import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return (

You clicked {count} times

setCount(count + 1)}> Click me
); } 次の状態を決定する処理を書く 初期値

Slide 15

Slide 15 text

React Hooks import React, { useState } from 'react'; function Example() { // Declare a new state variable, which we'll call "count" const [count, setCount] = useState(0); return (

You clicked {count} times

setCount(count + 1)}> Click me
); } この部分だけ再描画される

Slide 16

Slide 16 text

React Hooksはどうやって実現しているの? ● packages/react-reconciler/src/ReactFiberHooks.js ○ FiberはいわゆるComponentの内部状態を管理するためのもの ○ HooksはFiberのmemoizedStateに単方向リストとして保存 ● あるコンポーネントのHooksを処理するときは ○ memoizedStateのHooksを辿っていき、順に処理する → Hooksは必ずある一つのFiberに属する

Slide 17

Slide 17 text

React Hooksを書く時に必ず守らなければならないこと ● https://ja.reactjs.org/docs/hooks-rules.html ○ フックを呼び出すのはトップレベルのみ ○ フックを呼び出すのは React の関数内のみ ● つまり ○ 条件分岐の中で呼び出したり ○ ネストされた関数の中で呼び出したり → 使うHooksが一意ではない呼び出し方をしてはいけません

Slide 18

Slide 18 text

脆弱性を指摘できますか?

Slide 19

Slide 19 text

vs Component(props)

Slide 20

Slide 20 text

と Component(props) の違いは? ● == React.createElement(Component, …) ○ https://ja.reactjs.org/docs/jsx-in-depth.html ○ "JSX とは、つまるところ React.createElement(component, props, ...children) の 糖衣構文にすぎません。 " ○ Component(props)とは明確に異なる ● Hooksを管理する上でこの違いは非常に重要 ○ いま呼び出そうとしている関数に Hooksが含まれている可能性があるか?が非常に重要 ○ つまりComponent(props)は「フックを呼び出すのは React の関数内のみ」に違反する

Slide 21

Slide 21 text

通常の挙動 ()

Slide 22

Slide 22 text

通常の挙動 ()

Slide 23

Slide 23 text

通常の挙動 () App:useState(0) count: 0 Hooks List

Slide 24

Slide 24 text

通常の挙動 () App:useState(0) count: 0 Hooks List EvenComponent:useState("I'm Even") msg: I'm Even

Slide 25

Slide 25 text

通常の挙動 () App:useState(0) count: 0 Hooks List EvenComponent:useState("I'm Even") msg: I'm Even

Slide 26

Slide 26 text

通常の挙動 () App:useState(0) count: 0 Hooks List EvenComponent:useState("I'm Even") msg: I'm Even

Slide 27

Slide 27 text

通常の挙動 () App:useState(0) count: 0 Hooks List App:useState(0) count: 1 初期化せずに再利用 EvenComponent:useState("I'm Even") msg: I'm Even

Slide 28

Slide 28 text

通常の挙動 () App:useState(0) count: 1 Hooks List EvenComponent:useState("I'm Even") msg: I'm Even

Slide 29

Slide 29 text

通常の挙動 () App:useState(0) count: 1 Hooks List EvenComponent:useState("I'm Even") msg: I'm Even OddComponent:useState("I'm Odd") msg: I'm Odd 異なるコンポーネントなので初期化

Slide 30

Slide 30 text

通常の挙動 () App:useState(0) count: 1 Hooks List OddComponent:useState("I'm Odd") msg: I'm Odd

Slide 31

Slide 31 text

通常の挙動 () App:useState(0) count: 1 Hooks List OddComponent:useState("I'm Odd") msg: I'm Odd

Slide 32

Slide 32 text

No content

Slide 33

Slide 33 text

間違った実装 (Component({...}))

Slide 34

Slide 34 text

間違った実装 (Component({...})) App:useState(0) count: 0 Hooks List

Slide 35

Slide 35 text

間違った実装 (Component({...})) App:useState(0) count: 0 Hooks List App:useState("I'm Even") msg: I'm Even

Slide 36

Slide 36 text

間違った実装 (Component({...})) App:useState(0) count: 0 Hooks List App:useState("I'm Even") msg: I'm Even

Slide 37

Slide 37 text

間違った実装 (Component({...})) App:useState(0) count: 0 Hooks List App:useState("I'm Even") msg: I'm Even

Slide 38

Slide 38 text

間違った実装 (Component({...})) App:useState(0) count: 1 Hooks List App:useState("I'm Even") msg: I'm Even

Slide 39

Slide 39 text

間違った実装 (Component({...})) App:useState(0) count: 1 Hooks List App:useState("I'm Even") msg: I'm Even App:useState("I'm Odd") msg: I'm Odd 初期化せずに再利用

Slide 40

Slide 40 text

間違った実装 (Component({...})) App:useState(0) count: 1 Hooks List App:useState("I'm Odd") msg: I'm Even

Slide 41

Slide 41 text

間違った実装 (Component({...})) App:useState(0) count: 1 Hooks List App:useState("I'm Odd") msg: I'm Even

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

これってヤバいんですか? ● ヤバいです ● 次のようなコードを考えるとわかりやすい

Slide 44

Slide 44 text

これってヤバいんですか? ● ヤバいです ● 次のようなコードを考えるとわかりやすい スプレッド構文 オブジェクトの中身が展開される

Slide 45

Slide 45 text

つまり ● コンポーネントの使い方によってはimgタグの属性をコントロール可能 ● scale = { src: "x", onerror: "alert(1);" } →

Slide 46

Slide 46 text

このバグを整理すると ● 以下の条件が揃うと脆弱性となる ○ 攻撃者がある程度データの中身をコントロール可能な Stateがある(source) ○ Stateの内容によっては予期せぬ挙動を引き起こす箇所がある( sink) ○ 以上の2箇所のStateの取り違いが起こる箇所がある( confusion) ○ 取り違いが起こっても呼ばれる Hooksの数は変わらない(layout) source, sink, confusion, layoutという名前は一般的な用語ではありません ● 例 ○ クエリ文字列をオブジェクトに変換している( source) ○ スプレッド構文でwidth, heightをimgタグに展開している( sink)

Slide 47

Slide 47 text

このバグは検知可能ですか?

Slide 48

Slide 48 text

一応可能です ● Reactはすごいのであの手この手でバグる可能性があることを教えてくれる ○ Devモード ■ 呼ばれているHooksの種類が変わったら consoleを警告で真っ赤に染めてくれる ○ eslint ■ plugins:react-hooks/recommendedを足すと教えてくれる ■ ● 静的にも動的にも検出する努力をしている! ○ 本当に十分?

Slide 49

Slide 49 text

実は十分ではない ● Reactはすごいのであの手この手でバグる可能性があることを教えてくれる? ○ Devモード ■ 呼ばれているHooksの種類が変わったら consoleを警告で真っ赤に染めてくれる → Hooksの種類が変わらないと教えてくれない ○ eslint ■ plugins:react-hooks/recommendedを足すと教えてくれる → CRA (create-react-app) や Vite (create-vite) でOptional → 標準の設定では検知できない

Slide 50

Slide 50 text

実は十分ではない ● すべてのチェックをすり抜けるパターン: 例で挙げたやつ

Slide 51

Slide 51 text

なぜ十分でないのか? ● ESLintのルールが雑 ○ フックが必ずReactの関数内で呼ばれているか?はチェックしてくれる ■ Reactの関数内か? → 関数名しかチェックしない( Camelcase or useHookName) ○ React.createElementとして呼び出しているか?はチェックしてくれない ● 検知しにくい → いろんなプロダクトに潜んでいる可能性 ○ Reactで書かれたUIは複雑になりがちなので多分 OSSにもある ○ バグバウンティチャンス ○ これを検知するESLintとかを書いたら喜ばれるかも

Slide 52

Slide 52 text

問題となりやすい実装

Slide 53

Slide 53 text

Case 1: callbackの中で呼んでいる ● どちらのuseStateが先に呼ばれるか決定できない ● 二回目のrender時に逆転する可能性 ● eslint: 検知 ● dev: 検知 ● prod: 検知

Slide 54

Slide 54 text

Case 2: 条件分岐の中で呼んでいる ● useState ● useState でconfusion ● eslint: 検知 ● dev: 部分的に検知 ○ Hooksの種類が 異なる場合のみ ● prod: 検知不可

Slide 55

Slide 55 text

Case 3: 関数の中から呼んでいる ● 例で使ったパターン ● eslint: 検知不可 ● dev: 部分的に検知 ○ Hooksの種類が 異なる場合のみ ● prod: 検知不可

Slide 56

Slide 56 text

対策とまとめ

Slide 57

Slide 57 text

対策方法 ● 基本的に発火する条件が厳しい ○ source, sink, confusion, layoutがすべて揃うことが必要 ● 開発者が気をつけなければならない箇所: confusion ○ 他の条件は防ぎようがない ● 関数呼び出しは検出できないので気を付ける

Slide 58

Slide 58 text

まとめ ● React Hooksのルールを守りましょう ○ フックを呼び出すのはトップレベルのみ ○ フックを呼び出すのは Reactの関数内のみ ● (多分)いろんなところに潜んでいる ○ 是非探してみてください

Slide 59

Slide 59 text

参考文献 ● https://github.com/zwade/live-art/blob/master/solution/writeup.md ● https://github.com/facebook/react ● https://ja.reactjs.org/docs/hooks-rules.html ● https://github.com/acdlite/react-fiber-architecture ● https://ja.reactjs.org/docs/reconciliation.html ● https://sbfl.net/blog/2019/02/09/react-hooks-usestate/