
我盯着后台日志里那条被 URL 编码包裹的请求愣了几秒%3Cscript%3Ealert%28document.domain%29%3C%2Fscript%3E解码后是scriptalert(document.domain)/script。这意味着已经有人在拿我们域名试 XSS 了。前端安全这个东西平时看不见摸不着可一旦有人开始试探问题就摆到了桌面上XSS、CSRF、内容安全策略这些名词大家都听过但真正在工程里怎么防、怎么绕、怎么落地才是今天想跟你认真聊的内容。这篇算是一份实战复盘围绕 XSS 的三种形态、CSRF 的绕过思路、CSP 的配置与失效陷阱展开也会把 SpringBoot 全局过滤器处理上传文件时的 XSS 对抗、PDF 内置脚本这类冷门场景一起拉出来讲。文中我会穿插 DVWA、Pikachu、PortSwigger、CTFshow 等靶场里反复练过的攻击思路以及我在真实项目里踩过的坑。无论你是前端、全栈还是刚接触安全测试照着这条路走一遍基本能把常见 Web 攻击链条摸个七七八八。1. XSS三种形态与靶场复现XSS跨站脚本攻击本质上就是“把一段你不想执行的脚本塞进别人浏览器里执行”。它跟 SQL 注入有点像SQL 注入是把指令拼进 SQL 语句XSS 是把指令拼进 HTML/DOM。区别只在于注入目标不同但核心都是“不可信数据 不安全拼接”。1.1 存储型与反射型DVWA 与 Pikachu 里的经典戏法先拿 DVWA 这种老牌靶场练手。Low 级别下反射型 XSS 基本就是直接拼参数$name $_GET[name]; echo Hello $name;输入scriptalert(1)/script脚本直接被输出到 HTML 里执行。看起来很简单但你应该意识到反射型 XSS 的关键不在“能不能弹窗”而在“参数值是否完整经过了服务端再返回并且落到了可执行上下文中”。中高级题会加htmlspecialchars()或正则过滤目的是观察输出位置是否变化如果脚本被插进了input value...的属性里那闭合引号后就能构造事件属性比如img srcx onerroralert(1)这时候单纯过滤script就失效了这是靶场题最经典的区分点。Pikachu 靶场的存储型 XSS 和反射型很类似但有一个核心区别值得强调存储型是“一次提交永久生效”影响所有访问页面的人反射型是“一次请求一次性回显”需要诱导受害者打开带 Payload 的链接。在实际业务里反射型虽然不持久但配合短链接、钓鱼邮件、二维码照样能规模化成批打不要因为“不持久”就轻视它。我在真实项目里处理过一个案例搜索框的报错提示里直接回显了用户输入导致某个参数被插进页面文本中。因为输出点在label标签内其实很难直接构造脚本但参数里面放了img srcx onerror...之后照样能触发。这种位置判断能力比背 Payload 重要得多。1.2 DOM 型 XSS最隐蔽的前端盲区如果说存储型、反射型是服务端没编码好那 DOM 型 XSS 就完全是前端自己的锅攻击载荷压根不经过服务端直接由浏览器里的 JavaScript 处理 location.search、location.hash、localStorage、postMessage 等不可信来源再拼进 innerHTML、document.write、eval、href 等危险位置。PortSwigger 靶场有一道经典题前端代码大致长这样const params new URLSearchParams(window.location.search); document.getElementById(backlink).href params.get(url);这里url参数被直接拼进a的 href攻击者传一个urljavascript:alert(document.domain)用户点这个链接就会执行脚本。注意这个 Payload 在服务端日志里看起来只是普通参数WAF 甚至不容易匹配到特征——因为完整的恶意代码可能藏在#后面的 fragment 里而 fragment 根本就不会发到服务端。这就是 DOM 型 XSS 难拦的根源。CTFshow 上的 DOM 型 XSS 题目也喜欢考这个点考点通常在location.hash的取值、window.name传递数据、eval()执行字符串。实操中判断一个点是不是 DOM 型用浏览器直接改 URL 的#部分看前端行为变化即可不需要挂抓包工具。1.3 绕过编码、属性和标签攻击者的惯用招数靶场里能通关不代表实战能打进去。实战里 WAF、过滤器、开发框架一般都会做基础过滤所以真正绕的过程才是最值钱的部分。常见的绕过套路我整理成一张表绕过维度典型手法为什么能绕过编码绕过HTML 实体编码、Unicode 编码、URL 双重编码WAF 匹配的是明文特征而浏览器解析时分别在不同阶段解码标签绕过svg/onloadalert(1)、details open ontoggle...过滤名单通常只写了script而 HTML 允许执行事件属性的标签极多属性闭合img srcx onerror...、 autofocus onfocus...过滤时只转义了单引号或者输出位置在属性值内需要先逃逸伪协议javascript:、data:text/html;base64,...href/src 过滤不严或遗漏了大小写/编码变体旧特性CSS expression()、iframe srcdoc针对旧浏览器兼容代码常见现代浏览器已经不支持部分特性但仍然值得列防御侧看到这张表就该明白黑名单过滤永远是补不完的。正确的做法是按输出上下文选择编码函数HTML 标签里用实体编码属性值里用属性编码URL 里做 URL 编码CSS 里做 CSS 编码。OWASP 的 ESAPI 和前端侧的 DOMPurify 都是按这个思路设计的比你在代码里写一百个正则靠谱得多。2. CSRF从信任链裂口到绕过实录XSS 攻击的目标是“骗过浏览器执行代码”CSRF 攻击的目标则是“借浏览器的身份发请求”。两者经常被放在一起讨论因为它们都属于“浏览器机制被利用”的典型漏洞。2.1 三分钟看懂 CSRF 的信任模型CSRF 能成立依赖一个前提浏览器会自动携带 Cookie而且服务器默认信任这个 Cookie 就是用户本人在操作。我常拿门禁卡做类比攻击者不需要偷你的钥匙只需要趁你刷卡进门的时候跟在后面门卫认卡不认人就一起放进去了。具体流程画在脑子里受害者在 A 站已登录浏览器持有 A 站 Cookie攻击者构造一个恶意页面 B页面里放一个指向 A 站敏感接口的请求受害者访问 B 时浏览器带着 A 站的 Cookie 自动发出了这个请求A 站服务器看到“合法的 Cookie 合法的参数”就执行了操作。很多人以为只要用 JSON 接口、POST 请求就不会 CSRF这是误区。HTML 表单的enctype虽然默认是application/x-www-form-urlencoded但老浏览器和某些环境里跨域表单提交并不严格限制 Content-Type加上 CORS 配置如果允许了特定 Origin照样能把 JSON 载荷发出去。更重要的是很多早期接口为了兼容设计成 GET 参数那才是 CSRF 重灾区。2.2 绕过 CSRF 的常用姿势与案例复盘CTF 里和实战中的 CSRF 不太一样。CTFshow 和 dig2pen 这类环境通常会给你一个完整的“业务系统”让你去分析 Token 的生成、校验和上下文绑定关系常见绕过套路有四种2.2.1 Token 可预测或全局固定如果 Token 是个固定值或者只依赖时间戳、用户名等可预测因素那等于没有 Token。攻击者可以提前“领”一个 Token或者在攻击页里预置这个 Token。判断方法很简单退出登录再登录如果 Token 不变或者同一 Token 在多用户间复用漏洞基本就坐实了。2.2.2 Token 绑定不够放在 query 里或 Cookie 里把 Token 放在 URL 参数里是个坏习惯。Referer 会原样带上 URL一旦请求被第三方站点引用Token 就泄漏了。更隐蔽的问题是 Double Submit Cookie 模式——如果服务器只校验“请求参数中的 Token 等于 Cookie 中的 Token”而站点存在子域名 Cookie 投放或 XSS 时攻击者可以直接把恶意 Cookie 种到浏览器再带上这个值发请求。2.2.3 Referer 校验只看前缀很多系统校验 Referer 时写了if (referer.startsWith(https://example.com))。这个判断能被https://example.com.evil.com绕过因为字符串前缀确实匹配。正确做法是解析 URL 的hostname字段做精确比较而不是用字符串前缀。2.2.4 XSS 先打进去再掏 Token这个思路是降维打击CSRF 的防令牌确实能挡住“无脑请求”但如果攻击者已经通过 XSS 拿到了当前页面的 DOM完全可以自动读取页面里的 Token再发起带 Token 的请求。也就是说XSS 和 CSRF 经常是组合拳前者帮后者解除限制安全设计上不能把两者割裂来看。一个完整的 CSRF PoC 通常是这样的html body form actionhttps://victim.com/user/changeEmail methodPOST input typehidden nameemail valuehackerevil.com /form scriptdocument.forms[0].submit();/script /body /html如果受害者浏览器里恰好有 victim.com 的登录 Cookie就会直接执行改邮箱操作。2.3 防御落地的三种主流方案对比CSRF 防御没有银弹但方案已经很成熟方案原理优点坑Synchronizer Token服务端生成随机 Token 存入 Session表单提交时校验安全性高业界标准多服务器/集群需要共享 Session异步请求要手动带上Double Submit CookieToken 同时放 Cookie 和请求参数服务端只比对两个是否一致不需要服务端存储天然适配分布式对子域名和 XSS 的抵抗力弱SameSite Cookie 属性浏览器限制跨站请求携带 Cookie实现成本最低现代浏览器默认 Lax老浏览器不支持且 GET 形式的 Lax 也有少量风险我个人的落地习惯是关键接口用 Token SameSiteLax 双保险普通读取接口靠 SameSite 兜底登录态接口全部拒绝 GET 写操作。这里要特别提醒SameSite 不是万能的Lax 模式下顶层导航的 GET 请求仍然会带 Cookie所以涉及“修改类操作”的 GET 接口必须严格排查。3. 内容安全CSP 的完整落地与失效陷阱XSS 和 CSRF 讲完之后必须聊 CSP。因为 CSP 是前端安全里最该早配置、但最容易被配置坏的一道防线。它不能治本但能让攻击者在成功注入后的“执行阶段”举步维艰。3.1 从响应头开始CSP 语法与常见指令CSP 通过 HTTP 响应头或meta标签声明策略告诉浏览器“这个页面允许从哪里加载什么资源”。最简配置大概长这样Content-Security-Policy: default-src self; script-src self; object-src none; base-uri self拆开看default-src self所有没单独指定的资源类型只能从同源加载script-src self脚本只允许同源object-src none禁止加载插件内容比如 Flash、Java Applet——很多老攻击向量都靠它们base-uri self限制base标签防止攻击者篡改页面里的相对路径很多人以为 CSP 只需要script-src其实object-src和base-uri同样关键。object-src不设的话老版本浏览器里的 Flash 可以绕过部分 CSP 执行脚本base-uri不设的话攻击者注入一个base hrefhttps://evil.com/页面所有相对路径请求都会被带到攻击者服务器。这三个指令是配套的缺一个都有洞。配置 CSP 有两种方式响应头适合 Nginx、网关层统一配置meta适合页面级控制但注意meta方式对部分指令比如frame-ancestors是无效的而且report-uri在 meta 里也不建议依赖。3.2 严格策略三板斧nonce、hash 与 strict-dynamic现代前端工程里页面脚本基本都来自打包后的 JS 文件部分场景还会内联初始化脚本。如果 CSP 只写script-src self业务里的内联脚本和动态生成的脚本就全被拦了页面直接白屏——这也是很多团队 CSP 上线后被骂惨的原因。破解方法是 nonce 或 hashscript noncea1b2c3d4 initApp(); /script对应的响应头Content-Security-Policy: script-src nonce-a1b2c3d4; object-src none服务器每次响应生成随机 nonce只有带正确 nonce 的内联脚本能执行。工程化落地时在模板引擎里给script注入 nonce 属性即可成本不高。hash 方式适合纯静态页面把脚本内容哈希后写进策略脚本内容一变哈希就得跟着变动态页面维护成本稍高。strict-dynamic是个进阶选项一旦启用之前通过 nonce/hash 放行的脚本所加载的子脚本也自动获得信任不再依赖域名白名单。这解决了现代框架里动态插入script的需求但它要求你不能同时使用self和unsafe-inline否则会被降级解析安全效果大打折扣。3.3 CSP 也会失效绕过方式与补救措施CSP 不是万能保险。即使配了script-src self如果同源站点里存在 JSONP 接口攻击者可以构造script src/api/jsonp?callbackalert让恶意代码在 JSONP 回调里执行而这个请求来源是同源的CSP 会直接放行。PortSwigger 上就有专门一关考这个找到页面允许的 script 域名里包含 JSONP 端点利用回调参数绕过。另一个常见问题是白名单开后门。很多人配 CSP 时图省事直接script-src self https://cdn.example.com结果这个 CDN 上如果挂过漏洞页面或被托管了用户上传内容CDN 本身就变成攻击者的跳板。所以第三方脚本域名的信任要谨慎能限 path 就限 path能上 SRI子资源完整性校验就上 SRIscript srchttps://cdn.example.com/lib.js integritysha384-oqVuAfXRKap7fdgcCY5y4ACz8zY16w crossoriginanonymous/scriptSRI 保证脚本内容被篡改时浏览器拒绝执行这是给“远程脚本”上的最后一道锁。还是那句话CSP 的配置原则是“默认全禁按需放行”能不放unsafe-inline就一定别放放了基本等于把脚本拦截这一层的大部分努力白费。4. 纵深体系过滤器、安全编码与检测闭环单点防御永远不够。真正让我觉得“能放心上线”的项目都是把过滤、编码、CSP、监控串成了一条完整的链路。这一章讲工程落地重点放在两个真实场景上。4.1 SpringBoot 全局过滤器的设计与踩坑Java 服务端常用的第一道防线是全局过滤器我之前就在 SpringBoot 里实现过一个 XSSFilter核心思路是对请求参数做统一清理。先看一个相对完整的实现轮廓Component public class XssFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { filterChain.doFilter(new XssHttpServletRequestWrapper(request), response); } }包装器里重写getParameter、getParameterValues、getHeader对所有返回值做 HTML 转义和敏感关键字替换。这里有两个坑我必须提醒你第一过滤器会影响富文本业务。如果系统有新闻编辑、商品详情这种允许用户提交 HTML 的内容全局转义会把这些内容全变成一堆lt;实体产品功能直接报废。我的做法是对富文本接口单独放行内容入库前用白名单策略过滤DOMPurify 在同一套体系里是绝配而不是用全局正则去黑名单。第二getParameter拿不到 multipart/form-data 的 body。文件上传接口的额外字段如果也被过滤你需要重写getInputStream()。这个坑很容易被忽略你用 Postman 测?namescript能拦住换成multipart表单后参数全部绕过。只要沾到文件上传过滤器就必须同时处理输入流。过滤器的粒度也要考虑。静态资源完全没必要过这一层排除掉/static/**、/favicon.ico能减少无意义的性能损耗。另外建议记录过滤命中的日志每次拦截到可疑参数写一条 WARN 日志包括来源 IP、UA、原始参数值。这些日志在攻击溯源时价值极高。4.2 特殊战场PDF 上传文件里的 XSS 怎么防题目热词里有一条非常有意思“springboot项目全局过滤器处理上传pdf文件时xss攻击”。这个场景很多人没接触过但它特别值得讲因为我真的在项目里遇到过。PDF 文件本身可以内嵌 JavaScript。这不是玩笑PDF 规范里定义了 JavaScript 动作打开文档或触发某个动作时内置脚本就会执行。常见的触发点包括/OpenAction打开即执行和/AA附加动作。如果系统允许用户上传 PDF 并提供在线预览攻击者可以上传一个嵌了恶意脚本的 PDF用户点击预览后脚本就在当前 Origin 下执行了。从效果看这跟存储型 XSS 没有任何区别。防御思路分三层文件内容检测不能只靠过滤器字符串匹配要解析 PDF 内部结构检测是否存在 /JavaScript、/OpenAction、/AA 等关键字。用 Apache PDFBox 这类库读取文档目录能拦掉大部分恶意文件。响应头设置预览接口的响应强制加Content-Disposition: attachment让浏览器直接下载而不是内嵌预览这是最有效的“一刀切”。如果业务一定要在线预览就用 PDF.js 沙箱渲染同时关闭 PDF.js 的脚本执行能力。CSP 兜底给预览页面配script-src白名单就算 PDF 里脚本真的执行也会撞上 CSP 的墙。我在实际项目中三项都做了最后上线后再也没有收到过关于 PDF 预览的 XSS 告警。4.3 检测与应急一套可上手的安全审计方案防守方最怕的不是不知道漏洞而是不知道“漏洞在哪里”。我建议把检测常态化而不是上线前才突击检查。基本流程是先建靶场练手。DVWA 和 Pikachu 适合入门PortSwigger 的 Web Security Academy 难度梯度更合理我把里面的 XSS 和 CSRF 题目刷了两遍收获最大的不是 Payload而是“找到注入点后先确认上下文”的思维方式。CTFshow 的题目更贴近 CTF 比赛题型灵活适合拔高。再上工具。Burp Suite 抓包是基本功配合自建的 XSS 接收平台比如开源的蓝莲花项目做盲打验证如果发现疑似存储型 XSS直接插入一个带唯一标识的 Payload脚本一旦被触发平台立刻收到回连不用苦等人工确认。最后是上线前安全验收清单。我习惯给每个页面列一张测试表大概长这样测试位置注入上下文测试用例预期结果判断方法搜索框回显HTML 文本img srcx onerroralert(1)原样显示或转义看是否弹窗URL 跳转参数href 属性javascript:alert(1)拦截 javascript 协议点击链接看行为用户昵称展示标签内文本svg/onloadalert(1)无脚本执行看是否闭合属性异步加载接口DOM innerHTML带事件的 HTML 片段不渲染 HTML看 DOM 节点这张表最好固化成自动化用例挂到 CI 里。我目前的做法是每次发布前用 OWASP ZAP 跑一遍被动扫描再用自己写的几个关键接口断言做回归成本不高但能拦住大部分“改了老代码带出旧洞”的回归问题。5. 常见问题排查与避坑技巧实录最后把我在真实项目里碰到的高频问题整理成一个速查表基本覆盖了前面所有章节的坑。每一条都是踩过之后才总结出来的希望你不用再踩一遍。5.1 高频问题速查表现象根因解决方案富文本内容被转义成乱码全局 XSS 过滤器把 HTML 当成纯文本转义了对富文本接口做白名单处理用 DOMPurify 替代全局转义CSP 上线后页面白屏script-src 太严内联脚本没有 nonce/hash生成随机 nonce 注入模板或改用 strict-dynamic上传 PDF 预览时触发 XSS 拦截PDF 内嵌 JavaScript 被执行解析 PDF 内容检测 /OpenAction响应头改成 attachment用 PDF.js 沙箱异步请求带不上 CSRF Token页面初始化时 Token 没有注入到请求 header在 meta 标签里输出 Token全局 ajax 拦截器自动加 headerCSP 配置了白名单仍然被绕过白名单域名下存在 JSONP 或可被劫持的资源移除不必要域名非用不可时加 SRI 和 path 限制排查 JSONP 端点过滤器只拦了 query 没拦 body没有重写 getInputStream 处理 multipart 请求在包装器中同时处理参数和输入流5.2 我的几点心得与建议前端安全最容易被当成“上线前才想起来的事”但我在实际对抗中的体会是真正有效的安全建设发生在写每一行代码的那一刻。不要指望一个过滤器、一个 WAF 或一个 CSP 头就能兜住所有问题把“输入校验、输出编码、最小权限、纵深防御”这四句话刻进日常开发习惯里比什么都管用。另一个建议是把攻击者的视角练出来。不要只看自己写的代码要去找系统里每一个接收外部输入的地方问一句“这里如果被注入脚本会发生什么”。靶场练的是套路实战拼的是思维。当你看到location.search、innerHTML、eval、document.write这几个关键字能自动条件反射大脑报警前面的几千字才算真正消化成了你自己的东西。