☰
前端安全实战指南:从XSS到供应链攻击的全面防护
2026/10/22 19:57:21 网站建设 项目流程

1. 前端安全的攻击面到底有多大

1.1 前端为什么总被当成“最不重要”的一环

干前端这么多年,我见过太多团队把安全当成后端的事情。开会聊安全,提的最多是“接口鉴权”“数据库加密”“服务器防护”,很少有人认真看一眼自己打包出来的那个 JS bundle 里藏着什么。

但实际情况非常打脸。我在某公司做技术复盘时统计过,近两年能查到的安全事件里,有接近四成是从前端发起的,或者说前端是攻击链上最关键的一环。攻击者不需要攻破服务器,只需要在你网站的脚本里找到一个可注入的缝隙,或者在依赖树里埋一个恶意包,就能在用户浏览器里干很多事情:偷 cookie、劫持表单数据、伪造页面内容。

前端不安全,不是“用户体验差点”,而是整个系统被撕开一个口子。现代前端已经承担了大量业务逻辑,SPA里有路由、状态管理、本地缓存、第三方登录、支付回调,这些全是攻击面。前端如果只是“把接口返回的数据渲染出来”的展示层,那确实不重要;可今天的前端早就不是展示层了,它是一个完整的客户端应用。

我自己的习惯是,拿到任何项目,先不看交互设计、不看组件拆分,先找接入点:哪里渲染了用户输入、哪里写死了密钥、哪里用了innerHTML、哪里直接拼接 URL 参数到页面里。这些位置就是前端安全的主战场。

1.2 浏览器这个“运行环境”天然有攻击面

前端的代码不是运行在你自己的服务器上,而是下载到用户浏览器里执行的。这意味着三件事:

第一,代码完全裸奔。任何用户都可以打开开发者工具,看你加载了哪些脚本、发了哪些请求、带了哪些参数。混淆和压缩只能提高阅读门槛,没办法真正藏住秘密。

第二,运行环境是用户自己的设备,面对的是不可信的输入。地址栏的 URL 可以被拼出各种奇怪的值,localStorage 里的内容用户自己能改,请求可以被代理工具拦截并篡改。你维护的前端应用,随时要处理被攻击者精心构造的输入。

第三,第三方资源不可控。你的页面里可能引用了字体库、CDN 上的公共脚本、统计脚本、广告脚本,任何一个环节被劫持,整个页面的信任链就断了。以前有过公共 CDN 上的某个 JS 库被人上传恶意版本的事件,很多站点的用户就被重定向到了钓鱼页面,这事就是典型的前端供应链事故。

这三件事合起来就是一句话:前端安全就是在“代码不设防”和“环境不可信”的前提下,尽可能地把可控范围内的风险管住。很多开发者的误区是不知道哪些地方属于可控范围,也不知道哪些地方已经失控了。

1.3 盘点现代前端项目的攻击面清单

我整理过一份攻击面清单,每次接手新项目都会对照着过一遍。这份清单不复杂,但是覆盖了绝大多数前端安全问题的入口:

  • DOM 渲染入口:innerHTML、document.write、outerHTML、insertAdjacentHTML,这类直接解析 HTML 的 API 是 XSS 的高发区。
  • URL 参数处理:从location.search或location.hash中取值并用于渲染、跳转、埋点,容易产生反射型 XSS 和协议注入。
  • 表单与用户输入:搜索框、评论框、富文本编辑器、上传文件名,任何用户可控的字符串进入 DOM 或后端存储后再回显,都是风险点。
  • 本地存储:localStorage、sessionStorage、IndexedDB 里保存的 token、用户信息、业务缓存,一旦被 XSS 拿到,等于把身份凭证交给了别人。
  • 第三方依赖:npm 包和 CDN 资源,是当前前端最大的隐性风险源,后面我会专门展开。
  • 接口调用与鉴权:前端代码里写死的 AppKey、Secret、内部接口地址,以及 token 的存放方式。
  • 跳转与打开外部链接:window.open、location.href的跳转目标是否被用户输入控制,target="_blank"是否缺失noopener。
  • iframe 嵌入与交互:自己的页面能不能被别人 iframe 嵌入,刷卡劫持和点击劫持都跟这个有关。
  • CSP 配置:是否允许了不受控的脚本来源,是否允许unsafe-inline和unsafe-eval。
  • 错误信息与调试日志:生产环境是否泄露了堆栈、接口路径、内部字段名。

每次做代码评审,我会重点看这几个地方,花的时间不多,但往往能抓到几个值得修的问题。前端安全不是什么高深的技术,很多时候就是把这些入口一个个管住,别留裸奔的口子。

2. XSS:前端安全问题里的头号常客

2.1 XSS 仍然是最容易踩中的坑

跨站脚本攻击,也就是 XSS,放在第一优先级讲,不是因为它新,而是因为这些年我见过太多项目在同一个地方反复栽跟头。

XSS 的触发前提非常简单:用户的输入被当作代码执行了。最经典的场景是搜索框。用户输入一段内容,页面拿到这段内容之后,把它拼接到 HTML 里显示“您搜索的关键词是:xxx”。如果你直接拼接而不做任何处理,攻击者输入<img src=x onerror=alert(document.cookie)>,这段内容就会被浏览器当成 HTML 标签解析,onerror事件被执行,恶意脚本就跑起来了。

有人可能会说“现在的框架已经帮我转义了”。确实,主流框架在绝大多数情况下会自动转义数据绑定里的字符串。但问题出在那些“跳出框架保护”的 API 上:v-html(Vue)、dangerouslySetInnerHTML(React)、innerHTML、insertAdjacentHTML。很多开发者为了省事,在需要渲染富文本内容时直接用这些接口,又把服务端返回的 HTML 原样塞进去,一旦这段 HTML 里混入了<script>或者事件属性,整个页面就沦陷了。

XSS 的变体也不止一种。反射型是临时拼到 URL 上的;存储型的把恶意内容存到服务端,所有用户访问都会中招;DOM 型的是纯前端问题,恶意数据从不经过服务端,直接在前端代码里流转最终被 DOM API 消费,这种最难排查,因为日志里根本看不到异常接口请求。

我一个同事之前处理过一个问题:用户提交了一个包含<style>标签的评论内容,通过富文本编辑器保存后,其他用户打开详情页时页面样式被改了,整个布局错乱。当时还没触发到脚本执行,但如果攻击者换成<img onerror>或者引用外部脚本,事情就没这么简单了。

2.2 防御 XSS 不能只靠“转义一下”

防御 XSS 的核心原则是:永远不要信任用户的输入,永远不要信任任何即将进入 DOM 的字符串。

这句话执行起来分几个层面。

首先是输出编码。把用户输入渲染到 HTML 标签内、属性内、JavaScript 字符串内、CSS 内,每种上下文需要的编码方式都不一样。框架大部分时候帮你做了这一点,但你要知道它帮你做了什么,这样遇到v-html这类接口时才不会放松警惕。

其次是输入校验。对输入内容做白名单限制,比如用户名只允许字母数字下划线,URL 只允许 http/https 协议并且校验域名。这个属于第一道防线,不能作为唯一防线,因为校验规则总有想不到的特殊情况,比如以前很出名的javascript:协议注入,就是经过 URL 解析器的层层兼容,最终在一个属性里被执行。

再就是 CSP(内容安全策略)。我认为这是前端安全里被重视程度严重不足的防护手段。CSP 用 HTTP 响应头告诉浏览器:你这个页面只允许加载哪些来源的脚本、样式、图片,不允许执行内联脚本和eval。即便 XSS 注入了<script>,如果这个脚本不在白名单内,浏览器会直接拦住,不执行。这相当于给 XSS 攻击加了一层最后的保险。

CSP 的配置需要花心思,因为太严格会误伤业务,太宽松又形同虚设。我见过比较合理的起步配置是这样的:

Content-Security-Policy: default-src 'self'; script-src 'self' https://static.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'none'

这里面script-src只允许同源和指定静态域名,禁掉了unsafe-inline,线上的服务端渲染注入脚本会被直接挡住。object-src 'none'禁掉 Flash 之类的老古董。base-uri 'self'防止基础标签被篡改后导致后续所有资源相对路径被劫持。这几项对大部分业务来说都是可以接受的严格程度,但收效非常明显。

第三层才是消毒清洗。对于确实需要渲染富文本的场景,不能用“转义所有 HTML”的办法,那样会把格式全毁掉。这时候要用专门的 HTML 白名单清洗工具,比如 DOMPurify,它会把字符串解析成 DOM,然后按照允许的标签和属性列表过滤掉危险内容。

我实际项目中处理富文本的代码大致长这样:

import DOMPurify from 'dompurify'; const cleanHtml = DOMPurify.sanitize(rawHtml, { ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code', 'pre'], ALLOWED_ATTR: ['href', 'src', 'alt', 'title', 'target'], ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i });

加ALLOWED_URI_REGEXP是为了拦掉javascript:这样的危险协议,H5 的data:text/html也在列表里被过滤掉。这套配置在真实项目里跑了很久,评论和文章内容没有再出过问题。

2.3 实战排查技巧:怎么快速定位 XSS 风险点

接手老项目或者做代码评审时,我有一套很固定的排查流程。

先全局搜危险 API:innerHTML、outerHTML、insertAdjacentHTML、document.write、v-html、dangerouslySetInnerHTML。把这些出现的地方全部列出来,逐个看数据是从哪里来的。如果数据源是自己写死的常量字符串,问题不大;如果数据源是接口返回、URL 参数、本地存储,就要重点审。

然后看数据流转路径。前端的数据流转经常跨文件:A 页面收到接口数据存入 store,B 组件从 store 取出来渲染。你只看渲染处可能觉得“数据是内部流转的,应该安全”,但中间可能经过某个工具函数做了字符串拼接,又把用户可控的字段混进去了。

最后是动态测试。不用搭复杂的攻击环境,直接在浏览器控制台里选几个高风险输入点,输入一段无害的测试 payload:

<img src=x onerror="console.log(window.location.href)">

如果控制台打出了当前 URL,说明 XSS 已经触发了,只是比较隐蔽。

有一个我在实际项目里踩过的坑想特别提一下:前端框架的模板语法在某些场景下会被绕过。Vue 的模板插值默认会转义,但如果你把模板写在 HTML 文件里的<script type="text/x-template">或者<template>标签内,然后又被v-html渲染,那么模板里的内容等于被二次解释,转义保护就失效了。这类问题静态搜索不一定能找到,需要靠测试触发。

3. CSRF、点击劫持和其他容易被忽略的“老毛病”

3.1 CSRF 的现状:依然危险,但没那么容易利用了

CSRF 的历史很长,攻击原理也简单:用户在 A 网站登录了,浏览器里带着 A 网站的 cookie;攻击者在 B 网站构造了一个请求,诱导用户点击,浏览器带着 A 网站的 cookie 去请求 A 网站的某个修改接口,服务端看到 cookie 有效就认为请求可信,执行了操作。

以前的服务端接口很多是纯 cookie 认证的,CSRF 成功率非常高。现在情况好一些,因为主流做法是前后端分离,前端用 JWT 或自定义 token 做认证,token 放在Authorization头里,而跨站请求受同源策略限制,攻击者没办法读取也不方便构造自定义头。但依然不能掉以轻心,因为还有几种情况会绕过:

  • 某些系统保留了 cookie 会话机制,比如内网系统、旧版接口。
  • 业务接口为了兼容做成了application/x-www-form-urlencoded的 POST 提交,这类请求可以用隐藏表单自动提交来伪造。
  • 部分接口用了 GET 来做状态变更操作,比如“通过 URL 执行操作”,这种是最容易被利用的。

CSRF 的防御手段不复杂,核心是验证请求来源和携带随机令牌。验证来源可以用校验Origin和Referer头来实现,但 Referer 有时候会丢失,不能作为唯一依赖。随机令牌方案比较成熟:服务端给每个会话生成一个不可预测的 CSRF Token,前端在发请求时带上,服务端校验不匹配就拒绝。现在主流框架都有现成的中间件,不用自己从头造轮子。

我自己的习惯是:新项目一律使用自定义请求头方式来发 POST/PUT/DELETE 请求,比如X-Requested-With: XMLHttpRequest,服务端校验这个头的存在。跨域请求带自定义头会触发预检请求,攻击者构造的普通表单提交根本没有办法带上这个头。这个方法算是轻量又有效。

3.2 点击劫持:安全测试里最容易被忽视的一个点

点击劫持,典型的攻击方式是攻击者把目标网站嵌入到自己页面的 iframe 里,再用 CSS 把它透明化,叠在一个“看起来正常的按钮”下面。用户以为自己点了“领取奖励”,实际点到了目标网站的“删除账号”按钮。

这个问题的危害被低估了,因为利用场景需要精细的页面布局配合,但对一些操作步骤明确的站点,钓鱼成功率不低。

防御手段主要分两层。第一层是 HTTP 响应头X-Frame-Options: DENY或SAMEORIGIN,告诉浏览器这个页面不允许被嵌入到其他站点。第二层是 CSP 里的frame-ancestors指令,它比X-Frame-Options表达力更强,可以精确控制哪些域名可以嵌入你的页面。

对大部分不需要被第三方嵌入的应用,我的建议是直接禁掉:

X-Frame-Options: DENY Content-Security-Policy: frame-ancestors 'none'

如果业务上确实需要被嵌入到特定合作方页面,frame-ancestors可以写合作方的域名白名单,比全放开安全得多。

还有一个容易遗漏的点是页面上的敏感按钮需要二次确认,这是业务层面的兜底。即使点击劫持防御因为某些兼容原因失效,用户对“确认删除”“确认转账”的弹窗有意识,也会多一层保护。我在某系统里见过状态变更类接口没有确认弹窗,直接单次点击生效,就属于设计缺陷。

3.3 被忽视的 iframe、open 和跳转细节

顺着点击劫持往下说,iframe 还有一个特性:父页面可以通过脚本访问同域 iframe 的内容,而跨域的页面在某些条件下也会被部分访问。前端的安全边界,往往在 iframe 与父页面之间被模糊了。

更常见的一个问题是target="_blank"不带rel="noopener noreferrer"。这在老代码里非常常见。当你用target="_blank"打开一个新页面时,新页面可以通过window.opener访问你的页面对象,如果新页面被攻击者控制,它可以篡改你页面的 DOM,或者把用户重定向到钓鱼页面。现在的浏览器大部分默认带了noopener行为,但代码层面显式声明依然是安全的习惯,兼容老浏览器也稳妥。

跳转类的安全问题集中在location.href、window.open的地址来源。有些业务需要支持“点击通知跳转到详情页”,跳转 URL 可能是服务端下发或 URL 参数携带的。如果这个 URL 没有经过白名单校验,攻击者可以构造javascript:alert(document.cookie)或者data:text/html,<script>...之类的协议,诱导用户点击,一样能让恶意脚本在页面上下文中执行。

我的做法是封装一个统一跳转函数,任何 URL 经过它就强制校验协议和域名白名单:

const ALLOWED_DOMAINS = ['www.example.com', 'passport.example.com']; function safeRedirect(target) { let url; try { url = new URL(target, window.location.origin); } catch (e) { return; } if (url.protocol !== 'https:' && url.protocol !== 'http:') { return; } if (!ALLOWED_DOMAINS.includes(url.hostname)) { return; } window.location.href = url.toString(); }

这个函数虽然简单,但它把“跳转到哪”这个问题从“业务代码里到处贴”变成了“统一受控”,后续要加域名白名单也只需要改一处。

4. 供应链攻击与依赖安全:最大的隐性风险

4.1 你项目里的一半代码,都不归你管

谈到前端安全问题,我最想强调的就是依赖安全。一个中型前端项目,node_modules里几千个包很正常,这些包基本都不是你写的,有些是明面上的依赖,有些是依赖的依赖。你根本不知道哪个包在哪次小版本更新里动了手脚。

供应链攻击的思路也很简单:攻击者不直接攻击你的网站,而是入侵一个有很多人都在用的开源包,发布一个带恶意代码的版本,只要有人升级依赖,恶意代码就被装进他的构建产物里。恶意的代码可以做很多很隐蔽的事情:在构建时读取环境变量里的密钥,往页面的 HTML 里注入一段脚本,收集用户的输入数据并通过网络发出去。

我在某次排查里就遇到过一次典型事件:某个插件包在某个版本里增加了一段代码,检查navigator.userAgent如果是特定浏览器就跳转广告地址。因为那个包被业务直接引用,所有相关页面在那个时间段都出现过异常跳转。当时团队里没人往供应链攻击上想,查了好久才发现是依赖的锅。

4.2 防住供应链攻击的操作清单

防供应链攻击没有一招制敌的办法,全靠好几个层面的习惯叠加。

第一,锁定版本。package.json里用精确版本号而不是^或~,这样意外升到被污染的次版本的概率会小很多。同时启用 lock 文件(package-lock.json或yarn.lock),并且把它提交进版本库。lock 文件锁定的是一整棵依赖树的具体版本,光改package.json不一定能保证实际安装的版本一样。

第二,构建环境里设置最小权限。不要把私有仓库的凭证、生产环境的密钥写进 CI 配置或者 Docker 镜像里,更不要在前端代码仓库里保存任何真实密钥。那些需要在前端用到的标识性参数,要区分可公开的和真正敏感的,能放到服务端校验的就不要塞进前端包。

第三,做依赖审计。npm audit可以检查已知漏洞库,npm outdated可以看版本落后情况。这个不完美,因为审计跑的是本地已知漏洞库,新型漏洞不一定能覆盖到,但作为常规体检还是很有价值。

第四,用私有源和完整性校验。企业内部搭建私有 npm 源,先把包拉进去做检测,再让项目从私有源安装。同时可以开启npm install的完整性校验,确保下载下来的包内容和发布时一致。这一步治不了“包本身就是恶意”的问题,但能防住“下载源被劫持”的问题。

第五,也是我觉得很多团队最该做的:定期审查直接依赖里那些你可能没注意过的包,尤其是那些仅构建期使用、但权重很高的小工具。大牌框架被人盯着,反而相对安全,真正危险的是那些几万周下载量、作者就一个人的工具包。它可能是很长一段时间都没更新的老包,也可能是个刚出现的小众新包。看到这类包,我会特意去看它的 README、GitHub 仓库活跃度、最近几条 release 的变更内容。花不了几分钟,但能避免很多意想不到的坑。

4.3 给构建产物做一次“减重”和“安检”

依赖安全除了管来源,还有一个实际操作:审查最终构建产物里到底包含了哪些代码。

有一次我在一个新项目里看到控制台报了一个陌生库的错误,查了半天发现是某个工具函数库里带了一个完整的日期处理模块,而这个工具函数库是被另一个包间接依赖的。也就是说,一行简单的代码,拖进来一个几万行代码的库。这不一定是安全问题,但它确实扩大了攻击面——每一行你无法掌控的代码,都是潜在的不确定因素。

所以我建议定期做一次构建产物体积分析和代码内容抽查。用 sourcemap 分析工具看一下线上加载的 JS 文件里,每个第三方模块占了多少体积;抽查几个模块,快速确认它出现在产物里是不是符合预期的。如果一个老项目里躺着一个来源不明的包,尽早排查处置,别等出了事再去翻依赖树。

另外,前端代码打包后尽量不要直接用公共 CDN 加载关键的第三方脚本,尤其是那些需要控制 DOM 的库。公共 CDN 一旦被劫持或出问题,你的页面会被强插不可控脚本。把脚本下载到自己的静态资源域名上,配合子资源完整性(SRI)校验,用<script integrity="...">告诉浏览器“这个脚本必须是这个哈希值,否则不加载”,能最大程度避免公共依赖被替换的风险。

5. 敏感信息泄露:藏在代码里的“裸奔秘密”

5.1 前端代码里那些你以为藏得住的东西

所有公司都遇到过这类问题:某个前端项目里写着一个地图服务的 Key,后来这个 Key 被爬虫扛走了,对方拿它刷了几十万次接口,账算在你头上。

为什么会出现这种事?原因是前端代码本质上要交付给用户,里面所有的常量都是透明的。你写“这段代码放在 minified 的 bundle 里,别人应该找不到”,但实际 minified 只是把变量名缩短和压缩,字符串常量依然是明文的。别人用格式化工具展开,密钥一目了然。

需要重点自查的敏感信息包括:

  • 服务端 API 的完整内网地址,这类地址暴露以后会直接成为攻击者的扫描目标。
  • 各种第三方服务的 AppKey、Secret、Token,尤其是在客户端 SDK 初始化时用到的。
  • 数据库、缓存、消息队列的地址和账号密码。这个属于低级失误,但真实项目里见过不止一次。
  • 内部系统的字段名、接口结构、错误码含义,这些看似不敏感,但能为攻击者节省大量探测时间。
  • 泄露在日志上报里的堆栈信息,包含源码路径和变量名。

处理这些问题的原则只有一个:能不能不在前端出现的,就坚决不要出现。环境变量也不是万能的,因为构建时的环境变量最终会注入到产物里,不是真正的“隐藏”,只是换了个名字而已。

5.2 错误信息和调试日志是最容易被忽略的口子

很多前端项目上线前会清理console.log,但错误监控 SDK 和全局错误处理不一定清理干净。有一次我在一个生产环境页面上看到报错堆栈直接弹到页面右下角,里面带着一个完整的内网 API 路径,一看就是全局错误处理没有做生产环境的降级。

我一般采用的做法是区分环境配置:

const isProduction = import.meta.env.PROD; window.addEventListener('error', (event) => { if (isProduction) { // 只上报关键信息,不展示堆栈详情 reportError({ message: event.message.slice(0, 200), fileName: event.filename, // 可能为空,也需要脱敏 lineno: event.lineno }); } else { // 开发环境保留完整堆栈方便调试 console.error(event.error); } });

还有一个小细节:不要在错误上报的 payload 里带上完整的 location.href。因为 URL 里经常带着用户的敏感参数,如 token、手机号、搜索关键词。上报之前先做一下 URL 脱敏,把 query 参数里的可疑字段替换掉。

5.3 前端也能做“指纹与风控”

说点稍微进阶的:前端安全不只有“防别人攻击你”,还有“识别谁在攻击你”。

很多风控体系会依赖前端采集的设备指纹:canvas 指纹、WebGL 参数、浏览器语言、时区、屏幕分辨率、UA 等组合成一个特征字符串,配合 IP、账号行为数据,来判断当前请求是不是自动化工具或攻击脚本。

这里涉及一些敏感取舍的做法,但其中有一个值得推荐的思路:前端指纹不是用来识别单个用户身份的(那涉及隐私问题),而是用来标记异常模式和批量攻击的。比如某账号在短时间内用几十个不同的指纹登录,就有明显的脚本化特征;某个 IP 上的指纹大量重复,也说明可能是代理池在跑自动化。

这类能力不是小团队标配,但明确一个原则对任何人都适用:前端采集的任何数据都不可信,只能作为服务端风控的一个参考维度,不能作为唯一判据。攻击者完全可以伪造指纹、模拟真实浏览器行为,服务端还是要以自己的数据和规则为准。

我处理过的一个案例里,某系统的验证码滑块突然被大量绕过,排查到最后发现是前端把滑动轨迹上报给了服务端,服务端只校验了“轨迹是否平滑”这一个维度,攻击者用脚本模拟了一段物理轨迹就绕过了。之后的修复就是服务端引入多维度行为分析,不再单独信任前端上报的数据。

6. 让安全变成习惯:一套可落地的日常防线

6.1 给团队一份可抄作业的安全清单

前面聊了很多具体问题,落到日常开发里,最有价值的是一份能直接照着做的安全检查清单。我把自己团队里用的那份整理出来,它不是什么合规体系,就是管住最关键的几十项:

  • 代码全局搜索危险 API,出现一个处理一个,不允许搁置。
  • 所有用户输入渲染处,确认走的是框架默认转义流程,不走v-html和dangerouslySetInnerHTML。
  • 富文本内容必须过白名单清洗器。
  • 外部 URL 跳转必须过域名白名单校验。
  • 所有第三方依赖版本锁定,lock 文件进仓库。
  • 新增依赖前快速看一眼包的来源和活跃度迹象。
  • 构建产物里禁止出现任何形式的密钥和内部地址关键字。
  • 生产环境关闭调试日志,错误上报脱敏。
  • 服务端接口按需配置 CSRF 校验和来源校验。
  • 全站配置 CSP 响应头,至少包含default-src 'self'和frame-ancestors 'none'。
  • 上传文件的域名和主站域名隔离,防止上传的 HTML/SVG 文件在用户浏览器里执行。
  • 静态资源上传时做 Content-Type 校验和白名单,禁止上传 HTML 文件后以 HTML 类型被直接访问。

我之前在某个项目里就是按这条清单过了一遍,查出了两个高危点和一个中危点。高危点里有一个是富文本内容没有清洗直接渲染的,另一个是内网地址被写在构建常量文件里。都是老业务沉积下来的问题,发现之后修复并不难,难的是没人愿意系统地查一遍。

6.2 推荐常用的安全检测工具和检查思路

工具方面,我按“静态扫描、运行时检测、依赖审计、人工核查”四类来配:

静态扫描用 ESLint 的安全插件,它能帮助发现一些明显的问题模式,比如innerHTML的使用(配合项目规范可以设为 warning 或 error)、不安全的target="_blank"等。运行时检测可以用浏览器插件直接模拟常见攻击 payload,也可以给测试团队提供一份“前端安全冒烟用例”,每次发版都跑一遍。依赖审计用建设平台提供的开源漏洞扫描服务,不完美但值得纳入日常流程。

人工核查永远是最重要的一环,工具只能发现问题模式,真正的业务风险要靠人看代码逻辑来判断。我的经验是:新项目每次代码评审都要把安全检查清单过一遍;老项目每季度安排一次安全专项 review,每次全量开会把查出来的问题记录在案,规定修复时限。

6.3 从被攻击到恢复的应急思路

前端安全做不到 100% 防御,所以应急响应也要提前想一下。不用搞复杂,至少想清楚几件事。

第一,如果发现线上页面被注入了恶意脚本,第一时间要知道如何快速下线风险。最有效的手段是从服务端配置里临时封锁相关路径,或者上静态页面挡在前面,同时把能定位的注入源(是哪个接口出问题、哪个依赖被污染)找出来。

第二,被污染的前端资源要全部重新构建发布。如果问题出在某个 npm 包,要退回相邻的安全版本,并检查该包在这段时间发布的上游依赖链是否还有问题。

第三,审计影响面。这一步往往被忽视,但它才是决定事件严重等级的关键。恶意脚本运行了多久?采集了哪些数据?这些数据传到了哪里?有没有迹象表明某个特定用户的 cookie 已经被盗、账号已经被操作?这些问题的答案,直接影响后续的通报范围和整改动作。

我在处理这类事件时体会最深的一步就一个:别慌。前端出安全事件虽然吓人,但大多数情况下影响都可以控制,前提是你知道哪些日志还在、哪些链路还能追溯。所以平时构建发布留好时间戳和版本信息,前端监控平台里把错误日志和接口日志留够回溯时间,这些“看起来不紧急”的基础设施,真到出事的时候就是命根子。

6.4 安全不能只靠“一波整改”

项目常见的情况是:领导发话搞一波安全整改,团队突击查几天,把明显的问题修了,然后——就没有然后了。这样的整改,过三个月再看,一半的问题会重新冒出来。

原因很现实:业务在快速迭代,新代码在不断增加,老问题修了,新问题很快又会出现。前端安全不是一次性的需求,它是开发流程里的一个常驻环节,需要靠习惯而不是意志力来维持。

我个人比较推崇的落地方式,是把它融入已有的研发流程里。提测清单里加上安全自查项,代码评审模板里加上安全关注点,发布检查里加一条“生产环境无调试日志和敏感信息关键字”。这些都不需要额外开评审会,不增加多少时间成本,但长期坚持下来,项目整体安全水位会明显高于那些不做这些事的团队。

6.5 再讲两个身边的真实体会

最后说两个我从实际项目中带出来的体会,算是给这篇长文收个尾。

第一个体会:前端安全问题往往不是“技术多高深”,而是“流程上没人管”。我跟进过的几个出问题的项目,事后复盘几乎都能找到共同点——代码评审环节没人看安全点,依赖升级流程没人把关,上线前也没有安全检测步骤。技术方案再对,流程不兜底,早晚还是会漏。

第二个体会:要平衡好安全和业务迭代的关系。我不是“为了安全什么都不让上线”的守门员类型,太激进的安全策略在真实业务里根本没有生存空间。合理的方式是分级对待:高风险的范围,比如涉及用户支付、敏感数据展示、第三方登录的区域,安全要求做到最严;普通业务功能满足基本防护即可,别为了一个展示页面引入复杂的安全流程把团队拖垮。

在我个人的实际体验里,真正能给前端带来安全感的,其实不是某个工具,也不是某个框架特性,而是两个习惯:碰到用户可控的数据,先想想它会不会变成代码执行;碰到没来由的第三方依赖,先翻翻它的来源。把这两个习惯刻进日常开发节奏里,比任何“安全月”整改都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询