首页 / 资讯中心 / 文章详情

后端防了XSS,前端还要防吗?两者缺一不可

后端防了XSS,前端还要防吗?两者缺一不可 ★ FEATURED ARTICLE
后端服务已经加了防XSS前端到底还要不要加这个问题我每次做安全评审都会遇到而且几乎每次都要花不少口舌去解释。问的人往往不是不重视安全而是被“后端已经过滤了”这个前提给安慰住了觉得多加一道前端校验像是重复劳动。今天就把这个事掰开揉碎聊透。先说结论后端必须加前端也不能省。这俩不是二选一而是各管一段。前端要做的也不是把后端做的事再抄一遍而是去堵后端根本堵不住的那部分。什么部分是后端堵不住的往下看。1. 先搞清楚XSS攻击到底是在打谁受害者是浏览器里的用户很多人对XSS有个错觉觉得攻击者拿XSS是想攻击“网站本身”。其实XSS打的是“正在看这个网站的别的用户”网站服务器更像是被借刀杀人的那个工具。攻击者把一段恶意脚本注入到页面里然后等着其他用户打开这个页面脚本就在那个用户的浏览器里执行了。举一个最简单的例子某个评论区功能用户在输入框写了scriptalert(document.cookie)/script然后提交。如果后端不做任何处理原样存进数据库之后任何人打开这个页面浏览器都会把这段script当成页面脚本执行一遍。攻击者当然不会只弹个alert他会在脚本里写“把当前用户cookie给我发到某个地址”或者“把这个用户的登录态带到攻击者的服务器上”再进一步就是冒用身份操作、偷数据、发消息。这里必须先说透一个关键点浏览器是忠实执行HTML的它不关心这段内容到底是用户输入的“数据”还是开发者写的“代码”。它的解析器碰到script标签就会执行碰上onclick...属性就会监听事件。所以XSS的本质是“数据被当成了代码执行”而防护的核心就是不信任任何外部来的数据严格区分数据和代码。那后端加了防护是在防什么最常见的做法是针对用户输入做过滤比如把转成lt;把引号转成quot;或者匹配script这类标签直接拦截。这种做法能挡掉大部分“反射型XSS”和“存储型XSS”——只要恶意输入经过这个入口在存储或者输出的环节被转义掉了恶意代码就失去效果了。这也是为什么很多人都觉得“后端加了防护就够了”因为他们的认知里XSS就是靠“过滤用户输入”解决的。但这个认知漏了一大块就是DOM型XSS。DOM型XSS的攻击脚本根本不需要经过后端存储它完全发生在浏览器端的JavaScript运行过程中。比如某个页面从URL参数里取了个值直接塞进innerHTML赋值给某个div。攻击者构造一个恶意链接发给用户用户一点开URL里的参数在页面加载时被当成HTML解析脚本照跑不误。这个过程中后端服务器全程没参与你后端过滤得再严格也管不到浏览器里JavaScript的DOM操作那一层。所以把话说明白后端防护解决的是“别人往你服务器上存东西时要小心”前端防护解决的是“你自己浏览器里处理数据时要小心”。两件事阵地不同谈不上谁替代谁。2. 后端加的那道防线到底能挡住什么又挡不住什么聊到这儿有必要把后端防护的能力边界画清楚。现在常见的后端防护手段大概是这么几类。第一类是输入过滤。请求进来的时候后端检查请求体、查询参数里面有没有可疑内容比如script、javascript:这种关键字有就拦截返回400或者直接去掉标签。这是很多Web框架自带的默认策略比如一些Java生态的过滤器、PHP框架里的参数清洗类似的东西大家应该都见过。第二类是输出编码。存进来的数据不折腾它但在渲染页面模板的时候做转义。后端框架的模板引擎大多有自动转义比如把变成lt;。这样即使数据库里存了一条script...,它渲染出来也只是一段纯文本浏览器不会拿它当脚本执行。这个方案比输入过滤更稳因为它不在存储前改数据不破坏原始内容只在展示时做处理。第三类是加安全响应头。比如设置CSP内容安全策略告诉浏览器“这个页面只允许加载来自某些来源的脚本”哪怕被注入了一段inline脚本浏览器也会直接拒执行。再比如设置HttpOnly属性让JavaScript拿不到cookie就算脚本执行了也偷不走cookie。这几种手段合在一起确实能挡掉绝大多数“服务器参与”的XSS场景。存储型XSS、反射型XSS本质上都是脚本要经过服务器再回到浏览器服务器在进口和出口都做了处理脚本就没法存活了。但这里有几个后端无能为力的场景我列出来大家感受一下就清楚了。第一个场景就是刚才说的DOM型XSS。攻击者利用的是浏览器里JavaScript的innerHTML、document.write、eval这类操作。整个流程不涉及服务端数据存储后端看不到攻击载荷自然无从过滤。第二个场景是后端转义逻辑覆盖不到的前端代码路径。比如后端对模板里{{ comment.content }}这种位置做了转义但是前端某个单独用Ajax拿到的数据自己手动操作DOM绕过了后端模板渲染那后端的转义就完全管不到。第三个场景比较容易被忽略:前端框架处理富文本、iframe、链接地址这一类特殊内容的时候,不能简单整段转义,否则用户排版功能就废了。比如让用户填个人简介里的个人网站链接,javascript:alert(1)这种协议头后端未必会拦,就算拦了,还有各种编码花样绕过。这时候如果前端在设置href时不校验协议头,等于给攻击者开了一扇窗。所以后端防护不是不重要,它重要,它是底线,是纵深防御里不可缺的一层。但防线设在一道是远远不够的,尤其是面对DOM XSS这种“不在服务器跑一遍”的攻击路径,后端鞭长莫及。3. 前端到底要不要加防护:从前端的阵地反推必要性现在回到大家最关心的问题:后端都防了,前端还需要再做一遍吗?我的回答是需要,但你要知道前端加防护加的到底是什么。前端防护的核心价值,不在于“再过滤一次用户输入”,而在于“管好浏览器这一侧的数据流转和代码执行边界”。我建议按四个层面来理解前端要做什么.3.1 第一层:管住DOM操作,别让数据直接进HTML前端最常见的漏洞根源就是直接把数据拼进HTML。element.innerHTML userInput、document.write(urlParam)、$(#box).append(str),这些都是危险操作。正规做法是把数据当“文本”处理,用textContent替代innerHTML,或者用createElement创建节点、setAttribute设置属性值。给个对比就清楚了。假设从URL参数里拿了个userName,后端完全没参与这个过程。// 危险写法:数据直接被浏览器解析为HTML document.getElementById(welcome).innerHTML 欢迎 userName; // 安全写法:数据永远被当成纯文本 document.getElementById(welcome).textContent 欢迎 userName;用户构造一个userName xxxximg srcx onerroralert(1)的恶意链接,第一种写法会触发脚本执行,第二种写法页面上就是一行纯文本,浏览器根本不会去解析img标签。前端的职责就是在这种“数据在浏览器内部流转”的环节里,主动选择安全的API,这个选择权完全在后端影响范围之外。3.2 第二层:输入校验做“提前拦截”,不是为了防攻击而是为了防脏数据很多人觉得前端做输入校验就是为了安全,其实我理解它的主要价值更偏“体验和数据质量”。比如限制评论长度、限制联系方式格式、限制上传文件类型,这些多半不是为了对抗攻击者——因为攻击者完全可以绕过前端页面,直接构造请求打到后端。前端的这种校验拦不住真正有恶意的人。但前端校验也不是白做。一个很实在的理由是:它能拦截掉“大部分误触发的脏输入”。比如某个用户复制了一段带HTML标签的内容进评论框,如果前端校验时做了提示或者直接清洗,就能避免脏数据进入后续流程。另一个理由是新前端框架在很多场景下,把输入校验和渲染处理放在一起做,写起来也不费事,顺手就把数据格式给规整了。举个例子,一个邮箱输入框,前端校验不通过就不发请求,这种校验对安全来说意义有限,但对降低后端垃圾数据压力、提升用户体验,收益非常直接。3.3 第三层:框架自身的转义机制要用对,别主动绕过现代前端框架,像几大主流框架,默认在模板里插入变量时都会做转义。你写{{ comment }}或者{comment},框架自动把变量里的HTML实体转掉,不会当作标签解析。这是框架给的第一道安全红利。但日常开发里总有人为了某些功能主动关闭这道保护。比如React里有个dangerouslySetInnerHTML属性,光看名字就知道危险性;Vue里有v-html指令;别的框架也有类似“原样输出HTML”的接口。这些接口一定是要保留的,因为总有一些合法的富文本渲染需求。问题是很多人在不需要富文本渲染的时候也图方便用它,等于自己把框架的自动转义拆掉了。前端要做的事很简单:默认用文本插值,只有明确知道内容来源可信、并且确实需要渲染HTML时才用v-html这类能力。用之前先想想这个内容有没有可能被普通用户编辑,编辑后有没有做过白名单过滤。3.4 第四层:用CSP这类浏览器机制做兜底CSP不只是后端能加,前端也能配合。CSP的设计目的是告诉浏览器:这个页面里哪些来源的脚本是可信的,其余的统统拒绝执行。后端可以在响应头里设置,前端也可以用meta标签设置。虽然meta方式能力弱于响应头,但对一些没法改服务器配置的场景是有效的补充。CSP的具体作用用一个场景解释就特别直观。假设攻击者往评论区成功注入了一段script srchttps://evil.example/x.js,后端的输出编码漏了这个场景。如果页面设置了严格的CSP,规定“脚本只能从自己域名加载”,浏览器加载这个外部脚本时直接拦下来,攻击代码根本跑不起来。这就是CSP作为“最后一道拦截网”的意义——即使前面的过滤被绕过,浏览器侧还有一层独立的规则把关。很多团队实践下来,比较稳的CSP策略是:默认禁止所有外部脚本,需要时用self放行同源脚本,inline脚本能禁就禁。这会让一些老项目改造起来有点费劲,但安全收益非常大。4. 一次完整的XSS防护落地案例:前后端各加了什么讲完逻辑层面的分析,用一个具体的模拟场景串一遍整个落地方案,大家印象会更深。假设做一个带有评论功能的轻量问答社区,用户在页面里可以发表评论,评论会展示给所有访问者。这个系统里前后端都不做任何防护时,攻击者是这么打的:A同学提交一条评论,内容为scriptalert(xss)/script。后端原样存储,其他用户打开详情页时,这个脚本在所有人的浏览器里执行。如果把alert换成一段读取localStorage并发送到指定服务器的脚本,那就是一次真实的账户信息盗取事件。现在逐层加防护:4.1 后端参与的部分后端在存储和输出端都做了处理。评论写接口增加白名单过滤:允许p、strong这类安全的排版标签,禁止script、iframe、事件属性这类高风险元素,这属于输入过滤。评论的查询接口在渲染模板时,对不参与富文本的字段做HTML实体转义。评论内容因为允许部分富文本,走的是白名单后的“安全HTML”;评论人的昵称是纯文本字段,直接强制转义。响应头里加上Content-Security-Policy: default-src self; script-src self这样的策略,同时给cookie加了HttpOnly和SameSite属性。这一套做下来,大部分“刷新页面才触发”的存储型XSS和反射型XSS就能挡住了。但是注意,评论内容里使用白名单逻辑时,过滤规则永远存在被绕过的可能性。攻击者把img src1 onerroralert(1)里的onerror拆成on ERror或者用编码变形,白名单匹配不到位可能就漏了。更关键的是,如果页面里有一段前端JavaScript用location.search的某个参数动态拼接DOM,进行任何后端防护都与这个过程无关,攻击的入口压根不在服务器上。4.2 前端补齐的部分针对上述后端管不到的场景,前端做了这几件事。前端模板渲染评论内容时,默认使用文本插值,比如昵称、评论时间这些字段完全不让浏览器当成HTML解析。对于真正需要展示格式的富文本内容,组件里先跑一遍白名单清洗,再交给框架的富文本渲染接口输出,而不是把未经处理的原始字符串直接塞进去。页面里有几个敏感操作需要用到URL参数,前端做了一个通用的getSafeUrlParam函数:所有从URL里取出来的值,一律先用encodeURIComponent做编码;需要落到a.href时增加协议头检查,只允许http和https,其他直接置空。同时,前端在页面初始化时通过meta标签设置了一个最小化的CSP兜底,和主站响应头策略并不冲突。这两层都落地之后,这个系统从“服务器入口”到“浏览器内部渲染”都有防护,任何一个环节被绕过,另一层还能兜住底。所谓纵深防御,要的就是这层叠起来的效果。5. 常见误区与排查实录:那些年我和同事踩过的坑实际推进XSS防护的时候,有一些典型认知误区和实操问题反复出现,我这里挑高频的几个分享出来。第一个误区:“后端已经转了义,前端拿到的就是安全数据,可以直接拼HTML。”这是最能害死人的一个想法。后端的转义是“后端视角”的转义,一旦前端拿到数据后又做了一次解码、或者重新拼到innerHTML,后端转义的效果就被覆盖了。举个实际遇到的情况:后端返回的评论内容里已经把所有转成lt;,按理说渲染出来应该是安全的,但前端同学为了方便,在拿到数据后先做了一次HTML解码,再塞进富文本编辑器的预览区。这一解码,script重新变回
阅读完成 · 觉得有帮助?
咨询建站