1. 项目概述:从“小把戏”到“大麻烦”的Web安全双雄
在Web应用开发与安全防护的日常工作中,有两个名字总是如影随形,它们不像SQL注入那样直接“掏空”数据库,也不像DDoS那样声势浩大,却像潜伏在阴影中的刺客,利用用户对浏览器的信任,悄无声息地完成攻击。这就是XSS(跨站脚本攻击)和CSRF(跨站请求伪造)。我见过太多项目,前端做得炫酷,后端逻辑严谨,却在部署上线后,因为对这两种攻击的防御疏忽,导致用户数据泄露、账户被恶意操作,甚至整个站点的信誉崩塌。今天,我们就来彻底拆解这对“黄金搭档”,不光是讲原理,更要结合我踩过的坑和实战中的案例,把防御方案讲透,让你下次在代码评审或渗透测试报告里看到它们时,能一眼看穿本质,并知道如何根治。
简单来说,XSS是“往别人的页面里插自己的脚本”,而CSRF是“借用别人的身份发自己的请求”。一个核心在于“脚本执行”,一个核心在于“请求伪造”。理解它们,不仅是安全工程师的必修课,更是每一位前后端开发者在设计功能、编写代码时必须绷紧的一根弦。我们会从攻击者的视角出发,看看他们是如何利用这些漏洞的,然后再切换到防御者的视角,构建起从开发到部署的立体防御体系。无论你是刚入门的安全爱好者,还是有一定经验的开发者,这篇文章都能帮你建立起清晰、可落地的认知与实践框架。
2. 核心攻击原理深度拆解:信任是如何被背叛的?
要有效防御,必须先深入理解攻击是如何发生的。XSS和CSRF虽然经常被并列提及,但它们的攻击面、利用条件和最终目标截然不同。我们将它们拆开来看,你会发现,它们攻击的是Web安全模型中两个不同维度的“信任”。
2.1 XSS攻击:当浏览器执行了不该执行的代码
XSS的本质是攻击者将恶意脚本注入到可信的网页中,当其他用户浏览该网页时,其浏览器会执行这些恶意脚本。关键在于“注入”与“执行”。根据脚本注入和执行的持久性位置,XSS主要分为三类:反射型、存储型和DOM型。
反射型XSS是最常见也最“经典”的一种。攻击过程通常是这样:攻击者构造一个包含恶意脚本的URL,然后通过邮件、社交网站等方式诱骗用户点击。服务器接收到这个请求后,未经过滤或转义,直接将恶意脚本作为响应的一部分返回给用户的浏览器,浏览器将其当作页面正常内容执行。举个例子,一个搜索功能,URL可能是https://example.com/search?q=用户输入。如果后端直接拼接:<p>您搜索的关键词是:+用户输入+</p>,那么当攻击者输入<script>alert('XSS')</script>并诱使用户访问https://example.com/search?q=<script>alert('XSS')</script>时,这个脚本就会在用户的浏览器里弹窗。它的数据流向是:用户浏览器 -> 服务器 -> 用户浏览器。恶意脚本并不存储在服务器上。
存储型XSS的危害性更大,因为它具有持久性。攻击者将恶意脚本提交到网站的后端数据库(如论坛发帖、用户评论、个人资料昵称),之后任何浏览到包含该恶意内容的页面的用户,其浏览器都会执行该脚本。比如,一个博客评论系统,如果不对用户输入的评论内容进行过滤,攻击者提交一条包含<script>stealCookie()</script>的评论。此后,所有访问这篇博客文章的用户,在加载评论时都会执行这个窃取Cookie的脚本。它的数据流向是:攻击者 -> 服务器数据库 -> 所有受害用户浏览器。
DOM型XSS是一种比较特殊的类型,它的恶意代码执行完全发生在客户端的DOM解析环境,不经过服务器。攻击利用的是前端JavaScript对DOM的操作。例如,页面有一段JS代码:document.getElementById('content').innerHTML = window.location.hash.substring(1);,它把URL的hash部分(#后面的内容)直接写入了页面的innerHTML。如果攻击者构造一个URL:https://example.com/page#<img src=1 onerror=alert('XSS')>,那么当用户访问时,onerror事件就会被触发。这种攻击更难被传统的服务端WAF(Web应用防火墙)检测到,因为恶意负载根本不会发送到服务器。
注意:很多人认为用了前端框架(如React, Vue)就天然免疫XSS,这是误区。框架确实在默认情况下提供了很好的转义保护(例如React对
{}内的变量进行转义),但如果你使用了dangerouslySetInnerHTML(React)或v-html(Vue)这类故意绕过安全机制的方法,或者将未经验证的数据传递给eval()、setTimeout()等函数,XSS漏洞依然会产生。
2.2 CSRF攻击:借刀杀人的艺术
如果说XSS是利用用户对网站的信任,在网站上“种木马”,那么CSRF就是利用网站对用户浏览器的信任,让浏览器在用户不知情的情况下,“代替”用户向网站发起一个恶意请求。它的核心在于“伪造”。
想象一个场景:你登录了网上银行A(bank-a.com),并且会话Cookie还在有效期内。此时,你不小心访问了一个恶意网站B(evil.com)。网站B的页面上隐藏着一个自动提交的表单,或者一张自动加载的图片,其src指向银行A的转账接口:<img src="https://bank-a.com/transfer?to=attacker&amount=10000" width="0" height="0">。你的浏览器在加载这个图片时,会自动携带你登录银行A的Cookie,向银行A发起一个GET请求。银行A的服务器看到这个带有合法Cookie的请求,就会认为是你本人操作的,从而执行转账。这就是一次典型的CSRF攻击。
CSRF攻击成功的必要条件通常被称为“三个确认”:
- 登录状态确认:用户已经登录了目标网站(如银行),并且会话尚未过期。
- 请求可预测确认:攻击者能够推测出目标网站某个敏感操作(如修改密码、转账)的请求参数格式(URL、方法、参数名)。
- 浏览器自动携带凭证确认:目标网站依赖Cookie等浏览器自动携带的机制进行身份验证,且没有其他不可预测的令牌(如CSRF Token)进行二次校验。
与XSS不同,CSRF攻击中,恶意网站B无法直接读取银行A的Cookie(受同源策略保护),但它可以“借用”这个Cookie发起请求。攻击者的目标不是获取用户数据,而是以用户身份执行某个操作。
3. 防御体系构建:从编码到架构的层层设防
理解了攻击原理,防御就有了清晰的靶子。防御XSS和CSRF不是单一措施,而是一个从开发习惯到架构设计的系统工程。下面我将分层次、分场景地给出具体、可操作的防御方案。
3.1 XSS防御:关键在于“不信任”与“转义”
防御XSS的核心思想是:永远不要信任用户输入,对所有输出到页面的动态内容进行适当的处理。这需要前后端协同。
3.1.1 输入验证与过滤这是第一道防线,但绝不是唯一防线。原则是“白名单”优于“黑名单”。即,只允许符合明确规则的输入通过,而不是试图拦截所有已知的恶意模式。
- 场景:用户注册时的“用户名”字段。
- 操作:后端使用正则表达式进行白名单验证,例如只允许中英文、数字和下划线:
/^[\\u4e00-\\u9fa5a-zA-Z0-9_]+$/。对于富文本编辑器(如评论、文章内容),完全过滤HTML是不现实的,此时应使用严格的白名单标签和属性过滤库(如Java的Jsoup,Python的bleach)。 - 心得:过滤要在服务端做。前端验证是为了用户体验(即时反馈),后端验证是为了安全。攻击者可以完全绕过前端,直接构造请求发给后端。
3.1.2 输出编码/转义这是防御XSS最根本、最有效的手段。根据数据将要放置的上下文环境,进行不同的编码。
- HTML上下文:将数据放入HTML标签内部(如
<div>内容)或普通属性(如alt,value)时,需要对&,<,>,",'等字符进行HTML实体编码。例如,<转成<。大多数现代Web框架的模板引擎(如Thymeleaf, Freemarker, Django Templates)在默认情况下都会自动进行HTML转义。<!-- 错误示例:直接输出 --> <div>${userInput}</div> <!-- 如果userInput是 `<script>alert(1)</script>`,就会执行 --> <!-- 正确示例:框架自动转义或手动转义后 --> <div><script>alert(1)</script></div> <!-- 浏览器会将其显示为文本,而非执行 --> - JavaScript上下文:将数据放入
<script>标签内或事件处理器(如onclick)时,需要进行JavaScript编码。通常使用\xXX或\uXXXX形式的Unicode转义。// 错误示例 var userData = "${userInput}"; // 如果userInput是 `";alert(1);//`,就会闭合字符串并执行新代码 // 正确做法:使用JSON.stringify(它会自动处理引号和转义) var userData = ${jsonStringifiedUserInput}; // 注意:这里jsonStringifiedUserInput已经是JSON字符串,无需外加引号 - URL上下文:将数据作为URL的一部分(如
href,src)时,需要进行URL编码。<!-- 错误示例 --> <a href="https://example.com?redirect=${userInput}">点击</a> <!-- 如果userInput是 `javascript:alert(1)`,就会形成XSS --> <!-- 正确做法:严格验证协议头,只允许http/https,并对参数值进行URL编码 --> - CSS上下文:较少见,但也需注意。应对放入CSS的值进行编码。
3.1.3 利用内容安全策略(CSP)CSP是一个强大的后端HTTP头,它告诉浏览器只允许加载和执行来自哪些源的资源(脚本、样式、图片等),从根本上减少了XSS的攻击面。
- 如何设置:在服务器的HTTP响应头中添加
Content-Security-Policy。Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'; img-src *; font-src 'self'default-src 'self': 默认只允许加载同源资源。script-src 'self' https://trusted.cdn.com: 脚本只允许来自同源和指定的CDN。style-src 'self' 'unsafe-inline': 样式允许同源和内联样式(某些UI框架需要)。img-src *: 图片可以从任何地方加载。font-src 'self': 字体只允许同源。
- 实操心得:部署CSP建议分两步走。第一步,先使用
Content-Security-Policy-Report-Only头,只报告违规行为而不拦截,观察日志,调整策略。第二步,确认策略无误后,切换到强制的Content-Security-Policy头。这能避免因策略过严导致网站功能异常。
3.1.4 设置安全的Cookie属性对于通过XSS窃取的Cookie,我们可以通过设置其属性来增加攻击者利用的难度。
HttpOnly: 这是最重要的属性。设置后,JavaScript(document.cookie)无法读取该Cookie,只能由浏览器在HTTP请求中自动携带。这能有效防止XSS攻击者直接窃取会话标识。// 在Java Servlet中设置HttpOnly Cookie Cookie sessionCookie = new Cookie("JSESSIONID", sessionId); sessionCookie.setHttpOnly(true); response.addCookie(sessionCookie);Secure: 仅允许Cookie通过HTTPS协议传输,防止在明文HTTP中被窃听。SameSite: 这个属性是防御CSRF的利器,但对限制Cookie在跨站请求中的发送也有帮助。Strict或Lax模式可以阻止第三方上下文发起的请求自动携带Cookie。
3.2 CSRF防御:关键在于“不可预测性”与“同源验证”
防御CSRF的核心思想是:确保敏感请求是由源自本网站的、可信的页面发起的,而不是来自第三方网站。
3.2.1 使用CSRF Token(同步器令牌模式)这是目前最主流、最有效的防御方案。原理是为每个用户会话生成一个随机、不可预测的Token,在渲染表单(或任何可能触发状态变更的请求)时,将这个Token作为一个隐藏字段(对于表单)或自定义HTTP头(对于AJAX)嵌入。服务器在处理请求时,校验这个Token的有效性。
- 服务端实现:
- 用户登录或访问站点时,在服务器端(Session中)为其生成一个唯一的CSRF Token。
- 在渲染任何包含表单的页面时,将该Token输出到一个隐藏的
<input>字段中,例如<input type="hidden" name="_csrf" value="生成的随机Token">。 - 对于AJAX请求,可以将Token放在页面的
<meta>标签里,由前端JavaScript读取并设置为自定义请求头(如X-CSRF-TOKEN)。 - 服务器接收到POST/PUT/DELETE等非幂等请求时,从请求参数或头部取出Token,与Session中存储的Token进行比对。一致则通过,不一致或缺失则拒绝。
- 为什么有效:恶意网站无法预先得知或获取到这个Token(受同源策略保护),因此它构造的伪造请求中必然缺少有效的Token,服务器校验会失败。
- 注意事项:
- Token必须足够随机(使用密码学安全的随机数生成器),且与用户会话绑定。
- Token应是一次性的,或具有较短的有效期,并在使用后更新,以防止重放攻击。
- 确保Token只通过HTTPS传输,防止被中间人窃取。
3.2.2 校验请求来源(Origin/Referer Header)服务器可以检查HTTP请求头中的Origin或Referer字段,判断请求是否来自合法的源(即自己的网站)。
Origin头:对于跨域请求,浏览器会自动添加,指示请求的来源(协议+域名+端口)。对于同源请求,某些浏览器可能不发送。Referer头:表示前一个页面的地址。但注意,Referer可能被用户浏览器设置或代理服务器过滤掉,存在为空的情况。- 实现:在服务器端拦截器或中间件中,对于敏感操作,检查
Origin或Referer是否以自己网站的域开头。// 伪代码示例 String origin = request.getHeader("Origin"); String referer = request.getHeader("Referer"); if (origin != null && !origin.startsWith("https://your-domain.com")) { throw new CsrfException("Invalid origin"); } // 或者检查Referer - 局限性:这不是一个完美的方案。
Referer可能缺失或被篡改(虽然浏览器通常不允许JS修改Referer,但某些浏览器扩展或非浏览器客户端可以)。它通常作为CSRF Token方案的补充。
3.2.3 利用SameSite Cookie属性这是浏览器提供的一种从源头限制Cookie发送范围的机制,对防御CSRF有奇效。
SameSite=Strict: 最严格。Cookie仅在同站请求(即当前网站域下)中发送。这意味着如果用户从其他网站(如邮件链接)点击过来,即使已登录,首次请求也不会携带Cookie,可能导致需要重新登录。适用于极高安全要求的操作。SameSite=Lax(默认值): 宽松模式。在跨站请求中,仅对安全(HTTPS)的顶级导航(如链接点击)发送Cookie,而对子请求(如图片、iframe、AJAX)不发送。这平衡了安全性和用户体验。大多数情况下,Lax是推荐设置。SameSite=None: Cookie在所有上下文中发送,但必须同时设置Secure属性(即仅限HTTPS)。这是为了兼容一些需要跨站Cookie的第三方服务。- 如何设置:在设置Cookie的响应头中指定。
Set-Cookie: sessionid=abc123; Path=/; HttpOnly; Secure; SameSite=Lax - 实操心得:将关键会话Cookie设置为
SameSite=Lax或Strict,能极大地缓解CSRF攻击。对于现代浏览器,这几乎可以防御绝大多数传统的CSRF攻击。但是,它不能防御同源下的XSS攻击发起的请求(因为同源请求会携带Cookie),也不能防御某些特定的攻击场景(如“登录CSRF”)。因此,它应与CSRF Token结合使用。
3.2.4 要求用户进行二次验证对于特别敏感的操作(如转账、修改密码、修改邮箱),强制要求用户进行二次验证,例如输入登录密码、短信验证码、或使用U盾等。这虽然不是纯粹的CSRF防御技术,但能从业务逻辑层面增加攻击门槛。
4. 实战场景与工具链:在靶场和真实代码中演练
理论讲得再多,不如亲手实践。下面我将带你搭建一个简单的靶场环境,并分析真实框架中的防御机制,让你有更直观的感受。
4.1 使用DVWA/Pikachu靶场进行手动测试
DVWA(Damn Vulnerable Web Application)和Pikachu是两款非常经典的Web漏洞学习靶场,内置了XSS和CSRF的漏洞场景。
环境搭建(以DVWA为例):
- 最方便的方式是使用Docker。确保你已安装Docker和Docker Compose。
- 创建一个
docker-compose.yml文件:version: '3' services: dvwa: image: vulnerables/web-dvwa ports: - "8080:80" environment: - PHPIDS=off # 关闭PHPIDS以方便测试 volumes: - ./dvwa_data:/app - 在终端运行
docker-compose up -d。 - 浏览器访问
http://localhost:8080,按照页面提示完成安装(数据库设置等),默认登录账号/密码是admin/password。 - 在DVWA首页左侧,将安全级别设置为
Low,这样防护最弱,便于我们理解漏洞原理。
反射型XSS(Low级别)测试:
- 进入
XSS (Reflected)模块。 - 在输入框尝试输入
<script>alert(document.domain)</script>,点击提交。你会看到一个弹窗,显示当前域名。这说明脚本被执行了。 - 尝试绕过:切换到
Medium或High级别,DVWA引入了简单的过滤(如将<script>替换为空)。你可以尝试使用大小写混合、双写、或利用事件处理器(如<img src=1 onerror=alert(1)>)进行绕过。这个过程能让你深刻理解黑名单过滤的局限性。
存储型XSS测试:
- 进入
XSS (Stored)模块。 - 在留言板输入恶意脚本并提交。
- 刷新页面或让其他“用户”(你可以新开一个浏览器无痕窗口访问)查看该页面,脚本会自动执行。这模拟了攻击持久化的效果。
CSRF(Low级别)测试:
- 进入
CSRF模块。你会看到一个修改密码的简单表单。 - 观察URL,例如
http://localhost:8080/vulnerabilities/csrf/?password_new=123&password_conf=123&Change=Change#。攻击者可以构造一个类似的URL,诱使你点击。 - 你可以自己写一个简单的HTML页面
evil.html,放在本地,内容如下:<img src="http://localhost:8080/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked&Change=Change#" width="0" height="0"> <p>你刚刚可能被CSRF攻击了(如果已登录DVWA)</p> - 在已登录DVWA的同一个浏览器中,打开这个
evil.html文件。观察DVWA的密码是否被修改(可能需要重新登录验证)。这个实验直观展示了CSRF的威力。
4.2 现代框架中的内置防御机制分析
了解手动防御后,我们看看主流框架是如何帮我们自动化这些安全措施的。
Spring Security (Java) 中的CSRF防护: 在Spring Boot项目中,只要引入了spring-boot-starter-security依赖,CSRF防护默认是开启的。它使用同步器令牌模式。
- 原理:Spring Security会为每个会话生成一个CSRF Token,并期望在除
GET,HEAD,TRACE,OPTIONS之外的所有请求中(通常是状态修改请求),携带这个Token。Token可以放在_csrf请求参数中,也可以放在X-CSRF-TOKEN或X-XSRF-TOKEN请求头中。 - Thymeleaf模板自动集成:如果你使用Thymeleaf,在表单中添加
th:action属性后,Thymeleaf会自动为你添加一个名为_csrf的隐藏字段。<form method="post" th:action="@{/change-password}"> <!-- Thymeleaf会自动插入:<input type="hidden" name="_csrf" value="..."/> --> <input type="password" name="newPassword"> <button type="submit">修改密码</button> </form> - AJAX请求:你需要从
meta标签或Cookie中获取Token,并手动设置到请求头中。Spring Security默认会将Token放在名为XSRF-TOKEN的Cookie中,你可以这样处理:// 使用jQuery示例 var csrfToken = $("meta[name='_csrf']").attr("content"); var csrfHeader = $("meta[name='_csrf_header']").attr("content"); $.ajax({ url: '/api/data', type: 'POST', beforeSend: function(xhr) { xhr.setRequestHeader(csrfHeader, csrfToken); // 例如 X-CSRF-TOKEN }, // ... }); - 禁用CSRF:对于纯API服务(无状态,使用JWT等),你可能需要禁用CSRF。可以在安全配置中关闭:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 禁用CSRF保护 .authorizeRequests() // ... 其他配置 } }重要提示:除非你非常确定你的API调用场景不会受到CSRF攻击(例如,所有客户端都是你控制的移动App,且认证不依赖Cookie),否则不要轻易禁用。
Django (Python) 中的CSRF防护: Django的CSRF中间件(django.middleware.csrf.CsrfViewMiddleware)同样默认启用,使用同步器令牌模式。
- 模板中使用:在模板的表单标签内使用
{% csrf_token %}模板标签。
这个标签会被渲染成一个隐藏的<form method="post"> {% csrf_token %} <!-- 其他表单字段 --> <input type="submit" value="提交"> </form>input字段:<input type="hidden" name="csrfmiddlewaretoken" value="令牌值">。 - AJAX请求:你需要从Cookie中获取名为
csrftoken的Cookie值,并将其作为X-CSRFTOKEN请求头发送。Django贴心地提供了获取该Cookie的JS函数示例。 - 豁免CSRF:对于某些不需要CSRF保护的视图(如接收第三方Webhook的接口),可以使用装饰器
@csrf_exempt。
React/Vue (前端) 与XSS: 现代前端框架在默认情况下,通过数据绑定机制自动对动态内容进行HTML转义,这是防御XSS的第一道强大防线。
- React:在JSX中使用花括号
{}插入变量时,React会自动将其转义为字符串。只有使用dangerouslySetInnerHTML属性时,你才需要格外小心,确保其内容是安全的。// 安全:userContent会被转义 <div>{userContent}</div> // 危险:需要确保htmlString绝对安全 <div dangerouslySetInnerHTML={{__html: htmlString}} /> - Vue:模板中的双花括号
{{ }}和v-bind指令(缩写:)在默认情况下也会进行转义。只有使用v-html指令时,才需要你自行确保安全。
心得:在React/Vue项目中,XSS漏洞往往出现在错误使用上述“危险”方法、或直接将不可信数据传递给<!-- 安全:content会被转义 --> <p>{{ content }}</p> <!-- 危险:需要确保rawHtml绝对安全 --> <p v-html="rawHtml"></p>eval()、setTimeout()、innerHTML等场景。建立严格的代码审查流程,禁止随意使用这些特性,是至关重要的。
5. 高级话题与疑难排查:当基础防御失效时
即使我们做好了所有基础防御,在复杂的现实环境中,仍然可能遇到一些棘手的场景和高级攻击手法。了解它们,能让我们在安全设计上考虑得更周全。
5.1 XSS的进阶绕过与防御
攻击者不会止步于简单的<script>标签。他们会尝试各种奇技淫巧来绕过过滤。
基于字符编码的绕过: 过滤器可能只寻找<script>字符串,但攻击者可以使用HTML实体编码、JS Unicode编码等方式进行混淆。
- 攻击载荷:
<img src=x onerror=alert(1)>。这里的a等是alert(1)的HTML十进制实体编码。浏览器在解析HTML属性时会对其进行解码。 - 防御:进行输出编码时,必须根据最终的输出上下文进行编码。在HTML属性上下文中,即使输入看起来是编码过的,只要浏览器会解码,我们就必须在输出前,确保将
&等字符转义为&。同时,输入过滤应采用规范化和解码后再检查的策略。
利用SVG/HTML5新特性: SVG文件内可以包含JavaScript,某些对<script>过滤严格的系统,可能允许上传SVG图片。
- 攻击载荷:一个恶意的SVG文件内容。
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"> - 防御:对用户上传的文件进行严格的类型检查(不仅看扩展名,更要看魔数或解析文件头),并将上传的文件存储在独立的、不可执行脚本的域名下(使用CDN或静态资源服务器),并设置正确的Content-Type。对于图片,可以进行二次渲染(压缩、缩放)以破坏内嵌的脚本。
DOM型XSS与前端框架的盲区: 即使后端做了完美转义,如果前端JavaScript不当地使用了innerHTML、outerHTML、document.write(),或者将不可信数据传递给eval()、setTimeout()、new Function()等,依然会导致DOM型XSS。
- 案例:一个从URL获取参数并动态更新页面内容的功能。
// 危险代码 const productId = new URLSearchParams(window.location.search).get('id'); document.getElementById('product-info').innerHTML = loadProductInfo(productId); // 如果loadProductInfo返回了HTML字符串且包含恶意脚本... - 防御:
- 首选:使用安全的API。用
textContent代替innerHTML来设置纯文本。如果必须设置HTML,使用经过严格净化的库(如DOMPurify)进行处理。 - 避免:绝对不要将不可信数据拼接字符串后传给
eval()、setTimeout、setInterval的第一个字符串参数,或new Function的构造函数。如果必须动态执行代码,请使用其他架构。 - 框架最佳实践:在React/Vue中,严格遵守数据驱动视图的原则,避免直接操作DOM。
- 首选:使用安全的API。用
5.2 CSRF防御的边界情况与对策
“登录CSRF”攻击: 传统CSRF攻击针对的是已登录用户。但“登录CSRF”攻击的是登录过程本身。攻击者伪造一个登录请求,让受害者在不知情的情况下,使用攻击者控制的账号密码登录了目标网站。此后,受害者在该网站上的所有操作(如发帖、购物)都会记录在攻击者的账号下,可能导致隐私泄露或为攻击者“刷单”。
- 防御:在登录表单中也加入CSRF Token。因为登录请求通常也是状态变更操作(创建会话)。确保登录接口同样受到CSRF保护。
JSON API的CSRF防护: 对于接收JSON格式数据的API,传统的在表单中加隐藏字段的方式不适用。攻击者仍然可以构造一个Content-Type为text/plain或application/x-www-form-urlencoded的请求来提交恶意数据,如果服务器端没有严格校验Content-Type,可能会被绕过。
- 防御策略:
- 校验Content-Type头:服务器端严格检查请求的
Content-Type头是否为application/json。但这并非绝对安全,因为某些场景下(如CORS预检请求)可以伪造。 - 使用自定义请求头:这是更推荐的方式。让前端在发送AJAX请求时,添加一个自定义头(如
X-Requested-With: XMLHttpRequest)。由于浏览器同源策略的限制,普通HTML表单(<form>提交)或<img>标签发起的请求无法添加自定义头。服务器端校验该头是否存在即可。// 前端AJAX设置 fetch('/api/endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' // 自定义头 }, body: JSON.stringify(data) });// 后端校验(Spring Security 伪代码) // 可以在安全配置中要求特定路径的请求必须包含此头 http.csrf().requireCsrfProtectionMatcher(request -> { // 排除某些不需要CSRF的请求,如登录、公开API // 对于需要保护的API,检查是否有自定义头 return !request.getHeader("X-Requested-With").equals("XMLHttpRequest"); }); - 将Token放入JSON Body:虽然不常见,但也可以将CSRF Token作为JSON Body中的一个字段发送,服务器端从Body中解析并校验。这要求攻击者必须能预测Token,难度等同于传统表单方式。
- 校验Content-Type头:服务器端严格检查请求的
SameSite Cookie的局限性:SameSite=Lax是当前浏览器的默认值,极大地缓解了CSRF。但它并非万能:
- GET请求的CSRF:
Lax模式允许在跨站顶级导航(如点击链接)的GET请求中发送Cookie。如果某个敏感操作(如删除文章)错误地使用了GET方法,仍然可能受到CSRF攻击。因此,严格遵守RESTful规范,状态修改操作必须使用POST、PUT、DELETE等非GET方法,是防御CSRF的重要前提。 - 浏览器兼容性:虽然现代浏览器都已支持,但仍需考虑少量旧版本浏览器的用户。
5.3 渗透测试中的常见问题排查清单
当你负责一个项目的安全审计或收到一份渗透测试报告时,如何快速定位XSS和CSRF问题?以下是一个速查清单:
XSS漏洞排查点:
- 输入点:所有用户可控的输入(URL参数、表单字段、HTTP头、上传文件、WebSocket消息)。
- 输出点:这些输入被输出到的所有上下文(HTML正文、HTML属性、JavaScript代码、CSS、URL)。
- 过滤与编码:
- 后端是否对输入进行了白名单过滤或净化?
- 前端/模板引擎在输出时,是否根据上下文进行了正确的编码?(检查是否滥用
v-html,dangerouslySetInnerHTML,是否直接拼接字符串生成HTML或JS)。
- CSP:是否部署了Content-Security-Policy头?策略是否过于宽松(如存在
unsafe-inline,unsafe-eval)? - Cookie安全:关键Cookie(如会话ID)是否设置了
HttpOnly和Secure属性?
CSRF漏洞排查点:
- 状态变更端点:所有非GET的、会导致状态变化的API端点(POST, PUT, DELETE, PATCH)。
- 防护机制:
- 这些端点是否要求CSRF Token?Token是否随机、与会话绑定、一次性或有时效?
- 框架的CSRF中间件是否全局启用?是否有特定接口被错误豁免(
@csrf_exempt)?
- Cookie设置:会话Cookie是否设置了
SameSite属性(至少为Lax)? - 请求方法:是否有状态变更操作错误地使用了GET方法?
- JSON API:对于JSON API,是否校验了
Content-Type或使用了自定义请求头防护?
工具辅助:
- Burp Suite / OWASP ZAP:使用这些代理工具进行自动化的主动和被动扫描,可以快速发现常见的XSS和CSRF问题。
- 浏览器开发者工具:检查网络请求,查看Cookie属性(Application -> Storage -> Cookies),检查响应头中是否有安全相关的Header(如CSP,
Set-Cookie: HttpOnly; Secure; SameSite)。 - 代码审计工具:对于源代码,可以使用类似
Semgrep、CodeQL等工具,编写或使用现成的规则来查找可能存在漏洞的代码模式(如查找innerHTML,eval()的调用)。
安全是一个持续的过程,而非一劳永逸的状态。XSS和CSRF作为OWASP Top 10的常客,其原理相对经典,但攻击者的手法也在不断演化。作为开发者,最有效的防御是建立起“安全左移”的意识,在需求评审、架构设计、编码实现、代码审查、测试部署的每一个环节,都将这些安全考量融入其中。从写好每一行对用户输入进行编码的代码开始,从为每一个表单添加CSRF Token开始,你的应用就会变得坚固得多。