Fork me on GitHub

codeql挖掘React应用的XSS实践

近日研究了一下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
    12
    class 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
    10
    const 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
    2
    var x=document.createElement('div')
    x.innerHTML="user controllable data";
  • 直接设置href

    1
    2
    var x=document.createElement('a')
    x.href="javascript:alert(1)";
  • 参数注入(注入dangerouslySetInnerHTML的情况)

    1
    2
    3
    4
    5
    6
    let 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
4
override 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
2
3
4
5
6
7
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
)
}

针对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
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
import javascript

class ReactSetHrefSinks extends DataFlow::Node {
ReactSetHrefSinks() {
exists(JSXAttribute attr |
attr.getName() = "href" and attr.getValue() = this.asExpr()
)

}
}

class ReactSetHrefTracker extends TaintTracking::Configuration{
ReactSetHrefTracker() {
this = "ReactSetHrefTracker"
}

override predicate isSource(DataFlow::Node nd){
exists(|
not (nd.asExpr() instanceof ConstantExpr)
and not exists(nd.toString().toLowerCase().indexOf("icon"))
)
}

override predicate isSink(DataFlow::Node nd){
nd instanceof ReactSetHrefSinks
}

}



from ReactSetHrefTracker pt, DataFlow::Node source, DataFlow::Node sink
where pt.hasFlow(source, sink)
select source,sink

innerHTML/outerHTML/href

Sink只需要判断是存在PropWrite事件,写入的属性为innerHTML/outerHTML/href即可

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
import javascript

class InnerHTMLSinks extends DataFlow::Node {
InnerHTMLSinks(){
exists(DataFlow::PropWrite pw |
pw.getPropertyName().regexpMatch("(innerHTML|outerHTML)")
and pw.getRhs() = this
)
}
}

class InnerHtmlTracker extends TaintTracking::Configuration{
InnerHtmlTracker() {
this = "InnerHtmlTracker"
}

override predicate isSource(DataFlow::Node nd){
not nd.asExpr() instanceof ConstantExpr
}

override predicate isSink(DataFlow::Node nd){
nd instanceof InnerHTMLSinks
}
}



from InnerHtmlTracker pt, DataFlow::Node source, DataFlow::Node sink
where pt.hasFlow(source, sink)
select source,sink

一个针对DOMXSS的判断例子

这里用一个针对DOMXSS的判断来说明一下怎样充分利用DataFlow功能的isSource,isSinkisAdditionalTaintStep函数来追踪一个比较完整的数据流。

有网友发来一个邮件请求帮忙针对以下的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
<!DOCTYPE html>
<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
14
class 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
19
class 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
2
3
4
5
6
7
8
9
10
override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(CallExpr call |
(call.getCalleeName() = "unescape"
or call.getCalleeName() = "atob"
or call.getCalleeName() = "decodeURI"
or call.getCalleeName() = "decodeURIComponent"
) and succ.asExpr() = call and
pred.asExpr() = call.getArgument(0)
)
}

这里判断条件只判断了是否调用了unescape/atob/decodeURI/decodeURIComponent等方法,如果调用则方法的参数为前驱,方法的结果为后继,从而保证数据流流经这些函数仍然能够继续追踪。

综上,针对这种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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import javascript

class 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
)
}
}

class 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
)
}
}

class DocumentWriteTracker extends TaintTracking::Configuration{
DocumentWriteTracker() {
this = "DocumentWriteTracker"
}

override predicate isSource(DataFlow::Node nd){
nd instanceof LocationHashSource
}

override predicate isSink(DataFlow::Node nd){
nd instanceof DocumentWriteSinks
or nd instanceof InnerHTMLSinks
}

override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(CallExpr call |
(call.getCalleeName() = "unescape"
or call.getCalleeName() = "atob"
or call.getCalleeName() = "decodeURI"
or call.getCalleeName() = "decodeURIComponent"
) and succ.asExpr() = call and
pred.asExpr() = call.getArgument(0)
)
}
}

from DocumentWriteTracker pt, DataFlow::Node source, DataFlow::Node sink
where pt.hasFlow(source, sink)
select source,sink

参考资料