群里小哥把截图丢过来,我一看就明白他经历了什么:照着 Electron 官方 Quick Start 把模板项目拉下来,npm start 成功弹出了窗口,然后习惯性按 F12 打开 DevTools,控制台一整片红色。最上面那行是Content Security Policy of your site blocks the use of 'eval' in JavaScript。他问我:代码一行没改,为什么这么多报错?
这个问题几乎每个刚接触 Electron 的开发者都问过。只要你在网上跟着教程把官方安全清单里那段 CSP 示例贴进了 index.html,或者你用的脚手架在 HTML 里默认声明了严格的 Content Security Policy,这个报错就是一道必答题。它的含义很直白:你的页面策略不允许执行 eval 这类动态 JavaScript,而你的项目里有东西正在这么做。
这篇文章我不想只丢一句"把 unsafe-eval 加进 CSP 就行"这种敷衍的答案。我会把这个报错的来龙去脉拆干净,带你按可复现的排查思路找到到底是谁在用 eval,再给出不同阶段该采用的解决方案。适合谁读:正准备用 Electron 做桌面端的 Web 开发者,以及在脚手架项目里被这个报错卡了一上午的人。
1. 先把这个报错解剖清楚——你看到的到底是什么
1.1 报错出现的典型场景和几种表现
先说场景。这个报错最常见的出现方式有三种。
第一种,你跟着官方快速入门走,克隆 electron-quick-start 之后,因为读过一些 Electron 安全文章,顺手往 index.html 的 head 里加了一行 CSP meta 标签。然后启动项目,控制台开始刷红。
第二种,你用 vue-cli-plugin-electron-builder、electron-vite、electron-forge 这类脚手架初始化工程。很多脚手架模板默认就在 HTML 里声明了 CSP,或者底层构建工具的开发模式本身依赖 eval。于是第一次 npm run dev 就会看到这个报错。
第三种,你在官方快速入门的基础上集成 React、Vue、Angular 这类框架,框架运行时或者你引用的某个依赖库内部用了new Function、eval,被严格的 CSP 挡了下来。
表现也不完全一样。轻则页面正常显示,但控制台报错,热更新失效——你改代码,页面半天不刷新;重则页面直接白屏,或者某个功能按钮点了之后提示异常。为什么会这样?因为被拦截的 eval 可能发生在应用启动阶段,也可能发生在某个交互触发的代码路径里,拦截点不同,表现自然不同。
1.2 报错文本里藏着的三个角色:CSP、eval、页面
把报错原文拆开看,信息量其实很大。
Content Security Policy of your site,这里的"your site"指的是你用 loadURL 或 loadFile 加载的那个页面。CSP 可能来自三个地方:页面里的<meta http-equiv="Content-Security-Policy">标签、HTTP 响应头、以及 main 进程里通过session.defaultSession.webRequest.onHeadersReceived手动附加的响应头。它不是你 Node 代码报的错,也不是 Electron 内置策略报的,是你页面自己的安全策略把执行动作拦下来了。
blocks the use of 'eval',这里的 eval 是一个统称。CSP 规范里明确受script-src指令约束的动态执行方式包括:eval()、new Function()、setTimeout("字符串")、setInterval("字符串")。换句话说,不只是字面意义上的eval()函数,所有"把字符串当代码执行"的动作都算这一类。
in JavaScript,说明被拦截的主体是页面里的 JavaScript 脚本。至于你是谁、代码逻辑写得多漂亮,在这条策略面前不重要,只要你在执行字符串代码,策略就拦你。
1.3 这不是"代码坏了",是"策略拦了"
我见过很多初学者把这个报错当成代码 Bug 来排查,改半天逻辑,甚至怀疑是 Node 环境问题。实际上这是一个典型的认知顺序错误。
打个比方:小区保安不认识一个外卖员,把他拦在门口。外卖本身没坏,人也不是坏人,但门禁规则里写着"没有登记的访客不能进"。CSP 就是那道门禁。你的代码就是那个外卖员。代码本身的语法、逻辑完全正常,只是执行前被安全策略拒之门外。
怎么判断是"策略拦了"而不是"代码坏了"?看报错类型。真正的语法错误、类型错误会显示具体的SyntaxError、ReferenceError,带明确文件和行号。而 CSP 的拦截信息通常以Refused to evaluate a string as JavaScript或Content Security Policy ... blocks开头,强调是策略拒绝。这个区别很关键,它决定了你接下来的处理方向:不是埋头改 Bug,而是想清楚两个问题——这条策略我该不该加,这个 eval 到底是不是必须的。
2. CSP 和 eval 之间的恩怨,为什么偏偏是你中招
2.1 CSP 到底在管什么:把 CSP 理解成"门禁白名单"
CSP 的全称是 Content Security Policy,内容安全策略。它通过 HTTP 响应头或者 meta 标签声明,规定页面能加载哪些来源的资源。它的核心思路不是"黑名单",而是"白名单"——凡是没明确允许的,一律拒绝。
CSP 里指令很多,和你这个报错直接相关的就是script-src,它控制脚本的加载来源。常见的值有:
'self':只允许同源脚本。- 域名列表:允许特定域名下的脚本。
'unsafe-inline':允许内联脚本。'unsafe-eval':允许 eval 这类动态执行。
比如你在 CSP 里写了script-src 'self',那页面上所有<script>标签里直接写的代码都会被拦,外部引用的脚本必须来自同源。这种强约束看起来麻烦,但它存在的意义是防 XSS。
XSS 攻击的本质,是攻击者把一段恶意脚本注入了你的页面,让它在用户信任的上下文里执行。如果没有 CSP,只要你的代码里有一处拼接用户输入没过滤干净,攻击者就能塞进自己的脚本。有了 CSP,就算输入没过滤干净,浏览器端也会因为"这个脚本不在白名单里"而直接拒绝执行。这是 CSP 最核心的价值,也是为什么 Electron 官方强烈建议开启它的原因。
2.2 eval 为什么被单独点名:动态执行是安全黑洞
eval 的问题在于,它把字符串编译成 JavaScript 执行,等于在运行时凭空制造代码。这带来的风险有两层。
第一层是安全风险。XSS 攻击者最喜欢的通道就是 eval。你想想,如果攻击者能找到一处注入点,往字符串里塞一段恶意代码,然后用 eval 一执行,攻击面瞬间就展开了。CSP 里如果不默认禁止 eval,那防御就永远有后门。所以规范选择把所有动态字符串执行都列为高风险行为,默认一刀切禁止。
第二层是性能风险。eval 的代码是运行期才出现的,V8 这类 JavaScript 引擎很难对它做提前的编译优化,因为引擎在编译阶段根本不知道这段代码长什么样。这和直接写源码完全是两个优化难度。虽然现代引擎对 eval 做了一定的补救,但从工程角度说,能不用尽量不用。
你可能会觉得,我又没写 eval,凭啥拦我?这就是关键了。CSP 面对的是页面上所有脚本,它不区分是你自己手动写的eval(),还是 webpack 构建工具为了让模块加载更快而生成的 eval 包裹代码,或者是某个第三方库内部调用了new Function。只要你这个页面里有任何一处动态执行,没有'unsafe-eval'的 CSP 就会拦。
2.3 Electron 里的 CSP,和浏览器里的 CSP 有什么不一样
在普通浏览器场景里,CSP 基本靠服务端返回响应头,生产环境几乎没人依赖 meta 标签。但在 Electron 里,页面加载方式变了,CSP 的来源也随之变化。
Electron 应用有两种典型的页面加载模式:
- 开发模式:通过 devServer 加载
http://localhost:端口,此时服务端可以返回响应头,meta 标签也依然有效。 - 生产模式:通过
loadFile加载本地文件,协议是file://。这个场景下没有服务端,想靠响应头来设置 CSP 就不太可靠,meta 标签成了最主流的声明方式。
此外,Electron 还提供了一个相对进阶的思路:在 main 进程里用session.defaultSession.webRequest.onHeadersReceived拦截请求,动态给页面附加 CSP 响应头。这种做法的好处是 CSP 集中管理,而且可以根据开发、生产环境注入不同的策略。但它只适合 HTTP 加载方式,file://协议下响应头这一招经常会失效。这一点后文展开讲。
Electron 官方 Security Checklist 对 CSP 的态度非常明确:建议开启 CSP,并把unsafe-eval和unsafe-inline都关掉。这就是你报错最根本的来源——你可能贴的正是官方指导的 CSP,却低估了当前开发链路对 eval 的依赖。
3. 排查链路:到底是谁在你的页面里用了 eval
3.1 先看控制台的完整堆栈,而不是只看第一行
遇到这个报错,不要急着复制到搜索引擎。正确顺序是先自己在控制台排查一遍。
第一步,展开报错条目。Chrome DevTools 里报错信息下方通常有一行堆栈,你点开看它指向哪个文件和行号。如果指向的是http://localhost:端口/static/js/bundle.js:123:45这种打包后的文件,说明问题出在构建产物里。如果直接指向file:///你的项目路径/index.html:10,说明是页面初始化时触发。
第二步,看堆栈里出现的函数名。如果看到webpack__require、__webpack_require__之类,基本可以判断是 webpack 的开发模式运行时代码在搞事。如果看到vue.runtime.esm.js、react-dom.development.js这类框架内部文件,那多半是框架运行时或框架依赖的某个模块触发了动态执行。
这一步的目标不是立刻定位到精确代码,而是判断"凶手"的大致范围:你自己的源码、构建工具、还是第三方依赖。方向错了,后面会浪费大量时间。
3.2 找 CSP 声明:meta 还是响应头
确定"是谁在触发"之前,先确认"是谁在拦截",也就是 CSP 到底声明在哪。
操作路径很直接。打开 DevTools,切到 Elements 面板,选中<html>,然后在源代码里搜索Content-Security-Policy,看能不能找到<meta http-equiv="Content-Security-Policy">。如果 meta 里有,复制下来看script-src这一项。
如果没有 meta,切到 Network 面板,刷新页面,点最上面那个文档请求(通常是页面 URL 本身),在 Response Headers 里找Content-Security-Policy字段。如果找到了,看它的script-src值。
还有一种情况,响应头里没有,meta 里也没有,但报错依然存在。那大概率是 main 进程里注册了onHeadersReceived拦截器,动态附加了 CSP。这种 CSP 在 Network 面板里能看到,但你在页面源码里搜不到。
这里有个细节要注意:如果 meta 和响应头同时存在,浏览器会同时强制执行两个策略,限制范围取交集,也就是以更严格的那个为准。所以排查时要两个来源都查一遍,别只看了 meta 就下结论。
3.3 顺着报错倒查真凶:自己的代码、webpack,还是第三方依赖
找到 CSP 声明之后,重点来了——到底是谁在动态执行 JavaScript。根据我的经验,九成情况落在下面这三类里。
第一类:你自己的代码里写了 eval 或new Function。这种情况在 Electron 快速入门阶段其实不多见,多见于把服务端逻辑直接搬到前端时,图省事用了某些字符串拼接执行的写法。定位方法最简单,在 Sources 面板里按 Ctrl+Shift+F 全局搜索eval(和new Function(,找到后逐个确认。
第二类:webpack 开发环境的 devtool 设置。webpack 为了实现快速编译和热更新,开发模式默认把模块包裹在 eval 里执行,典型配置是devtool: 'eval'或devtool: 'eval-source-map'。这种方式的好处是每个模块独立、增量编译快,坏处就是需要'unsafe-eval'的 CSP 放行。如果你的脚手架项目在开发模式下报这个错,先去看 webpack 配置里的 devtool。这个原因在 Electron + Vue/React 项目里出现频率特别高。
第三类:第三方依赖。有些库在运行时用new Function动态生成函数。比如老版本的模板引擎、低代码平台运行时、某些 JSON 解析库、部分打包器运行时。这类问题最头疼,因为不是你直接写的代码,定位到之后还未必改得动。
快速验证某类嫌疑人是不是真凶的技巧:临时给 CSP 加上'unsafe-eval',刷新页面。报错消失,说明当前这条路径确实是 eval 导致的。然后按类别逐个去掉自己怀疑的库或配置,再次观察报错是否回来。这个过程能帮你精确锁定问题源。
4. 解决方案:三种思路,对应三种处境
4.1 快刀方案:在 CSP 里放行 unsafe-eval
最直接的解法,是在 CSP 的script-src里加上'unsafe-eval'。
以 meta 标签举例:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval'">这么一改,报错立刻消失,页面正常。为什么说这是"快刀"?因为它见效最快,但代价也最明显——CSP 对 XSS 的防护能力被削弱了一大截。动态字符串执行等于重新打开了安全后门,攻击者一旦找到注入点,可以轻易构造 payload 执行任意代码。
这个方案适合什么场景?适合你第一次遇到这个报错,不确定到底是谁在用 eval,先用它验证方向;也适合纯本地开发、不追求安全强度的个人小工具。但有个底线你必须清楚:真正的生产环境、面向用户发布的 Electron 应用,尽量不要带着'unsafe-eval'上线。如果你发现很多所谓"快速入门教程"最后都让你加这个,那是因为教程作者想让你最快看到效果,不代表这是最佳实践。
4.2 根治方案:把代码里的 eval 换成更安全的写法
如果你不想放任'unsafe-eval',那就得找到具体代码,把它换成不依赖动态执行的写法。
分几种情况:
第一,如果你的本意是解析 JSON 字符串,那根本不需要 eval。直接用JSON.parse()。eval 解析 JSON 是老一辈开发者的坏习惯,既慢又不安全。
第二,如果你真的需要动态执行一段用户输入的 JavaScript,比如你要做一个在线代码编辑器、一个低代码平台的表达式引擎。这种情况不存在"安全地 eval",你再怎么防都防不住任意代码执行的风险。正确的做法是重新设计隔离方案,把动态代码放到沙箱 iframe 或者单独的 Web Worker 里执行,让它在受限环境里跑,主窗口保持严格 CSP。
第三,如果是 webpack 开发模式的问题,打开 vue.config.js、webpack.config.js 或 electron-vite 的配置文件,把 devtool 从eval、eval-source-map改成cheap-module-source-map或source-map。代价是开发模式下编译速度会略微下降,但换来的是不再依赖 eval。这个调整对生产构建也有正面意义,因为在生产环境用source-map比 eval 模式更适合线上排查。
第四,如果是第三方依赖内部用了new Function,先查它的最新版本是否修复了这个问题;修复不了就看看有没有官方替代库;实在不行,用 patch-package 给依赖打补丁,把动态执行替换成静态实现。这个操作会提升你的维护成本,但总比把 CSP 打开一个口子要安全。
4.3 工程方案:开发环境放开,生产环境收紧
真正值得推荐给长期维护项目的,是让 CSP 在开发和构建两个阶段拥有不同严格程度:开发时为了调试体验暂时放行'unsafe-eval',生产构建时恢复严格策略。
具体实现有两种路线。
第一种,页面走 HTTP 加载,在 main 进程中拦截响应头动态注入 CSP。示例代码:
const { app, session } = require('electron') app.whenReady().then(() => { session.defaultSession.webRequest.onHeadersReceived((details, callback) => { const isDev = !app.isPackaged const csp = isDev ? "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'" : "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'" callback({ responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': [csp] } }) }) })app.isPackaged用来判断是开发还是打包后的生产环境。开发模式下script-src带'unsafe-eval',生产模式收紧。
第二种,页面走file://协议加载,响应头方案不可靠,就只能在 index.html 里通过 meta 声明 CSP。要让 CSP 随环境变化,需要在构建阶段动态注入不同的 meta 标签。无论 Vite 还是 Webpack,都支持 HTML 模板环境变量,根据process.env.NODE_ENV渲染不同的 CSP 内容。开发和生产用两套策略,效果和第一种殊途同归。
这个思路的优点是安全与开发体验兼顾。坏处是配置复杂度上来了,对新手来说一开始就搞这套容易劝退。但它才是做正式项目该有的姿态。
4.4 三种方案怎么选
我用一个表格总结三种方案的适用情况,方便你对号入座。
| 方案 | 改动成本 | 安全性 | 适合场景 |
|---|---|---|---|
| 放行 unsafe-eval | 最低,改一行 CSP 即可 | 弱,XSS 防御明显下降 | 本地调试快速定位问题、个人小工具 |
| 重构掉 eval | 高,取决于代码范围和依赖 | 高 | 正式发布前的强制动作 |
| 环境差异 CSP | 中,需要维护两套策略 | 高 | 跨平台正式项目、长期维护项目 |
我的建议是,初学者先用第一种快速让项目跑起来,理解报错机制之后再逐步迁移到第三种。但如果一开始就是冲着做正式项目去的,直接按第三种来,后面省事。
5. 从快速入门到生产发布:CSP 的进阶配置与躲坑经验
5.1 一份可以直接抄的生产环境 CSP
当你准备把 Electron 应用发布出去,CSP 就需要从"能跑"升级到"合理"。分享一份我实际项目里常用的严格版 CSP,你可以直接拿去做基础配置:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'">逐条拆解用意:
default-src 'self':兜底策略,所有未单独指定的资源默认同源。script-src 'self':脚本只能来自同源,禁止内联脚本,禁止 eval。这是安全性的核心。style-src 'self' 'unsafe-inline':稍微放宽的内联样式。Electron 里很多 UI 组件库会动态注入样式,严格禁止内联样式会带来一堆莫名的样式丢失问题,所以这一条我保留了'unsafe-inline'。注意unsafe-inline本身也有安全风险,但它是style-src域下的,危害远小于script-src下的同名项。img-src 'self' data::允许data:协议的图片。很多图标、占位图走 data URI,不放行会白屏。font-src 'self' data::同理,图标字体依赖 data URL。connect-src 'self':限制 XHR、fetch、WebSocket 的目标地址。如果你的应用要访问外部 API,把域名显式加进白名单,比如connect-src 'self' https://api.example.com。object-src 'none'、base-uri 'self'、frame-ancestors 'none'、form-action 'self':分别封死<object>、<base>篡改、iframe 嵌套、表单提交这些容易被攻击的边角。
5.2 file 协议、打包、脚手架:几个容易踩的隐藏坑
这一部分是我实践下来最容易踩坑、也最容易被教程忽略的地方。
第一个坑:file://协议下,响应头 CSP 经常无效。很多人从浏览器 Web 开发转过来,习惯用响应头管理 CSP,在 Electron 生产环境里用loadFile加载页面时就傻眼了:明明 main 进程里设置了onHeadersReceived,页面的 CSP 却不生效。原因就是file://协议下服务器这个概念不存在,响应头无处安放。所以生产环境走 loadFile 的项目,老老实实用 meta 标签,别折腾响应头。
第二个坑:开发模式走http://localhost时,CSP 的connect-src必须放行 HMR 的 WebSocket 连接,一般形如ws://localhost:端口。如果你在开发环境改了 CSP,发现页面能开但热更新反复断连、控制台刷 WebSocket 错误,大概率就是connect-src没放行 ws 协议。调试半天发现是这问题的时候,人会非常崩溃。
第三个坑:打包和 CSP 报错是两个独立的排查域。如果你在 Linux 上打包 Electron 遇到 fpm 相关的报错,那是打包工具链和系统依赖的问题,跟 CSP 没有任何关系。我之前见过有人在打包报错里排查半天 CSP,纯属方向跑偏。记住一个原则:运行期报错找页面策略,构建期报错找工具链,别混在一起。
第四个坑:Vue 3、React 这类现代前端框架配合 Electron 时,开发模式和生产模式的构建行为差异很大。开发模式下脚手架常常默认开启 eval 相关的 source map,生产构建却不一定。这就导致一种很诡异的现场:npm run dev 一切正常,打包出来的生产版本一打开就白屏、控制台 CSP 报错。原因就是开发环境没配严格 CSP,生产环境配了,而某些依赖只在生产代码里触发了动态执行。最好的规避方法就是从一开始就把开发和生产两套 CSP 都明确写出来,别让某一套处于"没配"的模糊状态。
5.3 我的习惯:先让功能跑通,再把安全策略收紧
说了这么多,分享一点我自己的做事习惯。
我通常把 Electron 项目的安全配置分成两个阶段。第一阶段是原型验证阶段,这时候我完全不纠结 CSP,怎么快怎么来,先把功能跑通,确认 Electron 的壳子、渲染进程、主进程通信这些链路没有问题。第二阶段是功能稳定之后,我才会把官方 Security Checklist 从头到尾过一遍,从严格 CSP 开始配置。
在第二阶段,每遇到一个被 CSP 拦截的资源,我都会问自己两个问题:这个资源是合理的吗?还是这个安全策略太严格了?很多人在第二阶段容易犯的错误,是一遇到报错就放宽策略。比如内联脚本被拦,不加思考就加'unsafe-inline';eval 被拦,随手加'unsafe-eval'。这么操作下来,CSP 等于没配。正确心态应该是:先把报错定位到具体模块,再判断是该改代码还是改策略。
最后给你一个排错小技巧。如果页面是通过 meta 标签声明 CSP 的,你可以直接在 DevTools 控制台里执行下面这行代码,临时移除 CSP,然后刷新页面,看报错是否消失:
document.querySelector('meta[http-equiv="Content-Security-Policy"]')?.remove()报错消失,说明问题确实出在 CSP 策略上;报错还在,说明有响应头 CSP 或者别的机制在起作用,再去查拦截器。这个操作只能用来本地调试,不能作为生产问题的永久解法,但它真的能帮你省下不少排查时间。
Electron 的路很长,CSP 只是第一道门槛。把这道门槛迈过去之后,你会开始理解桌面应用和纯 Web 应用在安全模型上的差异,也会慢慢养成"先理解再动手"的排查习惯。下次再看到类似的报错时,你就不是那个急着问"怎么关掉它"的新手了。