近日研究了一下codeql这个源代码分析工具,由于工作中接触到的React框架的web应用较多,往日人工审计源码挖掘XSS通常是在Webstorm中寻找dangerouslySetInnerHTML等调用点,人工工作量较为复杂且可能遗漏一些东西,所以便尝试能否用codeql来辅助挖掘React应用中的XSS。
React应用中常见的XSS类型
React应用的XSS的产生情况一般有:
- 调用dangerouslySetInnerHTML
1
2
3
4
5
6
7
8
9
10
11
12class Hello extends React.Component {
render() {
return <div
dangerouslySetInnerHTML={{html:'<img/src="x"/onerror="alert(1)"/>'}}
></div>;
}
}
ReactDOM.render(
<Hello name="World" />,
document.getElementById('container')
);
直接将恶意html渲染到DOM中;
- a标签的链接判断不严格
1
2
3
4
5
6
7
8
9
10const userWebsite = "javascript:alert('Hacked!');";
class UserProfilePage extends React.Component {
render() {
return (
<a href={userWebsite}>My Website</a>
)
}
}
ReactDOM.render(<UserProfilePage />, document.querySelector("#app"));
a标签对于链接缺少过滤,可以通过插入javascript类的url来实现oneclick XSS.
直接调用innerHTML/outerHTML
1
2var x=document.createElement('div')
x.innerHTML="user controllable data";直接设置href
1
2var x=document.createElement('a')
x.href="javascript:alert(1)";参数注入(注入dangerouslySetInnerHTML的情况)
1
2
3
4
5
6let r={...input}
return (
<div
...r
></div>
)
参数注入的情况在我们的业务中较为少见,因此重点考虑前四种XSS情形的挖掘方式。
codeql实现
codeql一个重要功能便是能够跟踪数据流,我们只需要编写继承于TaintTracking::Configuration
类的数据流设置类,定义好isSource
,isSink
,isAdditionalTaintStep
这几个方法即可,其中isSource
表示数据流的源头,isSink
表示数据流流向目标,isAdditionalTaintStep
表示额外的连接数据流的判断。
对于React应用而言Source点其实不是很容易确定,而且盲目设置Source来源于location.hash/XHR如果isAdditionalTaintStep
函数设置不好的话很难能达到预期结果。因此我仅仅判断了Source是否是常量的情形。
dangerouslySetInnerHTML
Source 定义如下1
2
3
4override predicate isSource(DataFlow::Node nd){
not (nd.asExpr() instanceof ConstantExpr)
and not exists(nd.toString().toLowerCase().indexOf("icon"))
}
这里的第一行的判断便是判断Source点是否是一个常量类型的表达式,而第二行则是由于dangerouslySetInnerHTML经常被应用于加载某些svg图片,我们需要确定一下Source是否仅仅是一个图标类型的变量。
Sink则只需要判断这个节点是否是一个jsx的标签属性即可,实现如下1
2
3
4
5
6
7
8 class ReactDangerousSetInnerHTMLSinks extends DataFlow::Node {
ReactDangerousSetInnerHTMLSinks() {
exists(JSXAttribute attr |
attr.getName() = "dangerouslySetInnerHTML" and attr.getValue() = this.asExpr()
)
}
}
对于dangerouslySetInnerHTML的情况,我们还需要注意判断对于html属性的写操作,因为dangerouslySetInnerHTML的html属性才是真正的恶意输入点,否则codeql语句判断数据流时可能会停止到{html:xxx}
这个语句,不会继续跟进xxx
经过了哪些数据流。因此这里的isAdditionalTaintStep
方法定义如下
1 | override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
针对dangerouslySetInnerHTML的codeql语句最终如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import javascript
class ReactDangerousSetInnerHTMLSinks extends DataFlow::Node {
ReactDangerousSetInnerHTMLSinks() {
exists(JSXAttribute attr |
attr.getName() = "dangerouslySetInnerHTML" and attr.getValue() = this.asExpr()
)
}
}
class ReactSetInnerHtmlTracker extends TaintTracking::Configuration{
ReactSetInnerHtmlTracker() {
this = "ReactSetInnerHtmlTracker"
}
override predicate isSource(DataFlow::Node nd){
not (nd.asExpr() instanceof ConstantExpr)
and not exists(nd.toString().toLowerCase().indexOf("icon"))
}
override predicate isSink(DataFlow::Node nd){
nd instanceof ReactDangerousSetInnerHTMLSinks
}
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::ObjectLiteralNode obj, DataFlow::Node html_value |
obj.hasPropertyWrite("__html", html_value) and
succ = obj and
pred = html_value
)
}
}
from ReactSetInnerHtmlTracker pt, DataFlow::Node source, DataFlow::Node sink
where pt.hasFlow(source, sink)
select source,sink
isAdditionalTaintStep方法也可以继续优化,加入是否经过了Dompurify过滤、是否经过编码转换函数等。
a标签判断不严格的情况
只需要对上面的例子做更改即可,可以只修改Sink将属性改为href,去掉isAdditionalTaintStep
方法,这里的实现如下
1 | import javascript |
innerHTML/outerHTML/href
Sink只需要判断是存在PropWrite事件,写入的属性为innerHTML/outerHTML/href即可
1 | import javascript |
一个针对DOMXSS的判断例子
这里用一个针对DOMXSS的判断来说明一下怎样充分利用DataFlow功能的isSource
,isSink
,isAdditionalTaintStep
函数来追踪一个比较完整的数据流。
有网友发来一个邮件请求帮忙针对以下的DOMXSS例子编写codeql查询:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<html>
<head>
<meta charset="utf-8">
<title>DoraBox - DOM_XSS</title>
</head>
<body>
<form action='' method='GET'>
name:
<input type='text' name='name' id='form1'>
<input type='submit' name='submit' value='submit'>
</form>
<hr>
<script type='text/javascript'>
function getURLValue(name){
var reg = new RegExp('(^|&)'+ name +'=([^&]*)(&|$)');
var r = window.location.search.substr(1).match(reg);
if(r != null){
return unescape(r[2]);
}else{
return "";
}
}
document.write(getURLValue('name'));
</script>
</body>
</html>
这里的XSS例子是从location.search中取出内容来输出到网页上。
针对来自url的DOMXSS,我们可以按如下的方式定义Source:1
2
3
4
5
6
7
8
9
10
11
12
13
14class LocationHashSource extends DataFlow::Node {
LocationHashSource() {
exists(CallExpr dollarCall, PropAccess pr |
this.asExpr() instanceof CallExpr and
(dollarCall.getCalleeName() = "split"
or dollarCall.getCalleeName() = "substr"
or dollarCall.getCalleeName() = "substring"
) and dollarCall.getReceiver() = pr
and (pr.getBase().toString() = "window.location"
or pr.getBase().toString() = "location")
and this.asExpr() = dollarCall
)
}
}
这里主要判断是否存在函数调用,其中调用了split/substr/substring等方法,且这些方法的Receiver为window.location/location.
Sink点比较简单,直接参考上面React应用XSS的判断即可,这里定义了innerHTML和document.write两种类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class DocumentWriteSinks extends DataFlow::Node {
DocumentWriteSinks() {
exists(CallExpr call|
call.getCalleeName() = "write"
and call.getReceiver().toString() = "document"
and this.asExpr() = call.getArgument(0)
)
}
}
class InnerHTMLSinks extends DataFlow::Node {
InnerHTMLSinks(){
exists(DataFlow::PropWrite pw |
pw.getPropertyName().regexpMatch("(innerHTML|outerHTML)")
and pw.getRhs() = this
)
}
}
而为了能够保证经过unescape/decodeURI等编码类函数数据流仍然不断开,需要编写isAdditionalTaintStep
函数来添加额外的数据流判断
1 | override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
这里判断条件只判断了是否调用了unescape/atob/decodeURI/decodeURIComponent等方法,如果调用则方法的参数为前驱,方法的结果为后继,从而保证数据流流经这些函数仍然能够继续追踪。
综上,针对这种DOMXSS的codeql语句实现如下:
1 | import javascript |