首先问自己几个问题:知道什么是XSS吗?XSS有哪几类?如果你是一个网站攻击者,你会用XSS怎么攻击网站?XSS又如何预防?
如果你能清晰地回忆起所有概念和技术细节,那么恭喜你可以退出了。如果你还有所疑问请接着往下看。我会出一些例子来具体说明。最详细的综合案列在最后面说!
什么是XSS
XSS (Cross-Site Scripting) 是一种攻击类型,攻击者将恶意的脚本代码注入到其他用户信任的网页中。当受害者访问这个被注入了恶意脚本的网页时,脚本会在受害者的浏览器中执行。因为脚本是在目标网站的上下文中执行的,所以它可以所很多事情。比如:窃取用户的会话 Cookie、Token 等凭证,导致攻击者能完全控制用户账户。
那么XSS攻击有几类呢?
存储型 XSS
存储型 XSS 的攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。这种比较容易理解:我在论坛网站上发布一个
反射型 XSS
反射型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。 反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。
DOM 型 XSS:
与前两种不同,DOM 型 XSS 的漏洞完全存在于客户端的 JavaScript 代码中,不涉及服务器。
恶意脚本的注入和执行是由于前端 JavaScript 代码(如 innerHTML, document.write, eval, location.href, setTimeout 等)不安全地处理了用户可控的数据(如 URL 片段、表单输入)导致的。
攻击流程通常也是通过诱使用户点击恶意链接。
关键点: 恶意负载在数据到达服务器之前就被浏览器端的 JavaScript 解析并执行了。
注意:反射型XSS和DOM型XSS的共同点都是通过诱导用户点击某个链接将url的特定参数解析后在目标网站上执行获取用户信息。不同点在于,DOM型XSS的url上特定参数是不会传到服务的,是客户端(前端)自身的问题。常见的是通过url传递参数,然后用来获取默认值或者传递某个用户可控制的参数用来显示在页面上
一个案例解释DOM型XSS:
xml
<!DOCTYPE html>
<html>
<body>
<input type="text" id="expression" readonly>
<div id="result"></div>
<script>
// 1. 从 URL 片段 (# 后面的内容) 获取表达式
var userInput = decodeURIComponent(window.location.hash.substring(1)); // 例如:访问 #1+1 会得到 "1+1"
// 2. 将表达式显示在输入框 (这里使用了不安全的 innerHTML)
document.getElementById('expression').innerHTML = "计算: " + userInput;
// 3. 尝试计算并显示结果 (这里使用了极其危险的 eval)
try {
var calcResult = eval(userInput); // ⚠️⚠️⚠️ 极度危险的操作!直接 eval 用户输入!
document.getElementById('result').innerHTML = "结果: " + calcResult;
} catch (e) {
document.getElementById('result').innerHTML = "错误: 无效表达式";
}
</script>
</body>
</html>
在这个案例中使用了eval来执行Js计算值导致可以直接注入JS脚本:
less
// 假设上面代码的请求地址是这样的,参数还是通过描点
https://example.com/calculator.html#';alert('XSS');//
这样就可以直接注入攻击,就算你不使用eval函数,直接使用innnerHtml,我也可以:
xml
https://example.com/calculator.html#<img src=x onerror=alert('XSS')>1
直接注入标签的形式让onerror回调立即执行...所以慎重选择innerHtml,在这个案例中完全可以不使用它,这里只是举例说明。
综合案例
一个案例带你感受XSS攻防两端的魅力。
一天,在一个电商公司的前端开发Q仔根据公司业务需求开发了一个页面。根据url参数决定关键词的内容:
typescript
<input type="text" value="<%= getParameter("keyword") %>">
<button>搜索</button>
<div>
您搜索的关键词是:<%= getParameter("keyword") %>
</div>
几天后,Q仔收到安全团队发来的一个神秘链接,内容为:
xml
http://xxx/search?keyword="><script>alert('XSS');</script>
Q仔觉得安全团队找上门。肯定不是好事,心里忐忑的点开了...
明晃晃的alert带着XSS的字样出现在了Q仔眼前。完了,有漏洞。Q仔没慌,研究后发现: 当浏览器请求 http://xxx/search?keyword="><script>alert('XSS');</script> 时,服务端会解析出请求参数 keyword,得到 "><script>alert('XSS');</script>,拼接到 HTML 中返回给浏览器。形成了如下的 HTML:
xml
<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
您搜索的关键词是:"><script>alert('XSS');</script>
</div>
input标签被闭合,紧跟着的事一个script标签,带着它的恶意代码就执行了。同样div标签也被闭合了,这段恶意代码还会执行两次。
Q仔开发多年,这点事儿难不倒他,利用escapeHTML:
less
<input type="text" value="<%= escapeHTML(getParameter("keyword")) %>">
<button>搜索</button>
<div>
您搜索的关键词是:<%= escapeHTML(getParameter("keyword")) %>
</div>
转义后:
xml
<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
您搜索的关键词是:"><script>alert('XSS');</script>
</div>
只不过浏览器会转义回来按照正常字符来显示,问题完美搞定了。Q仔还在其他需要转义的地方都做了处理,开始躺平。
不久后,安全团队又发来一个链接:[http://xxx/?redirect_to=javascript:alert('XSS](https://link.juejin.cn?target=http%3A%2F%2Fxxx%2F%3Fredirect_to%3Djavascript%3Aalert('XSS "http://xxx/?redirect_to=javascript:alert('XSS")')
这个页面里有这样的代码:
ini
<a href="<%= escapeHTML(getParameter("redirect_to")) %>">跳转...</a>
反射执行后就变成了:
xml
<a href="javascript:alert('XSS')">跳转...</a>
不会立即执行,但是点击就完了...
Q仔心想:那就不让它以javascript开头就好了:
ini
// 禁止 URL 以 "javascript:" 开头
xss = getParameter("redirect_to").startsWith('javascript:');
if (!xss) {
<a href="<%= escapeHTML(getParameter("redirect_to"))%>">
跳转...
</a>
} else {
<a href="/404">
跳转...
</a>
}
安全团队再给一个:[http://xxx/?redirect_to=jAvascRipt:alert('XSS](https://link.juejin.cn?target=http%3A%2F%2Fxxx%2F%3Fredirect_to%3DjAvascRipt%3Aalert('XSS "http://xxx/?redirect_to=jAvascRipt:alert('XSS")')
Q仔裂开来,马上处理全部转成小写来判断,这下你没招了吧。
安全又来一个:[http://xxx/?redirect_to= javascript:alert('XSS](https://link.juejin.cn?target=http%3A%2F%2Fxxx%2F%3Fredirect_to%3D%2520javascript%3Aalert('XSS "http://xxx/?redirect_to= javascript:alert('XSS")')
%20javascript:alert('XSS') 经过 URL 解析后变成 javascript:alert('XSS'),这个字符串以空格开头。这样攻击者可以绕过后端的关键词规则,又成功的完成了注入。
Q仔内心:我现在辞职还来得及吗?这是被针对了啊!不过后来冷静之后想到了一个办法:
加入白名单:http,https。
ini
// 根据项目情况进行过滤,禁止掉 "javascript:" 链接、非法 scheme 等
allowSchemes = ["http", "https"];
valid = isValid(getParameter("redirect_to"), allowSchemes);
if (valid) {
<a href="<%= escapeHTML(getParameter("redirect_to"))%>">
跳转...
</a>
} else {
<a href="/404">
跳转...
</a>
}
问题彻底解决了,Q仔也经受住了考验。
深层启示:XSS防御需覆盖输入、输出、传输、执行四层链条,且须持续迭代。"XSS是认知战,开发者需理解数据流动的每一处缝隙,而攻击者永远在寻找下一扇窗。