Skip to content

lodash安全问题引发的思考

前段时间,公司在做审计,检测到我们后台的一个安全问题,一波三折才算是把问题解决掉,问题表现如下

alt text

alt text

强制使用最新的lodash版本

起初认为是版本太低了,所以我就重新安装了最新的lodash版本,并且强制所有的隐式依赖都用这个版本。

  • STEP1 升级之后,我先列了lodash的版本npm list lodash --depth allalt text
  • STEP2 我强制所有的隐式依赖都用这个版本的lodash
json
{
  "dependencies": {
    "lodash": "4.17.21"
  }
}

这时候自信满满去做提交,发现还是不行,又被打回了。

使用社区版本

于是我把错给了google, 发现使用的element-ui的框架上有一个issue, 于是就使用了社区的方法,使用

bash
npm install element-ui-ce

再次提交后,果真解决了。

根因分析

是因为element-ui引入了 4.17.10的死代码,导致了安全问题,并且将 _ 变量挂载到了全局(这个是次要的问题),导致了安全问题。

看了element-ui源码

  • 位置`lib/statistic.js,528行中有引用

alt text

  • 然后我们再看下引用的lodash文件 格式化后第8行,我们可以看到版本是4.17.10

这时候答案就呼之欲出了,是因为webpack构建的时候把lodash的版本也打包到了全局,导致了安全问题。所以你怎么升级都不起作用。

webpack原理

至于为什么lodash 会挂载到全局呢?因为我看代码逻辑,工程代码里不存在 AMD 的用法,所以window._ 打印出来应该是undefined才对。

js
  if (typeof define === 'function' && _typeof(define.amd) === 'object' && define.amd) {// Expose Lodash on the global object to prevent errors when Lodash is
    // loaded by a script tag in the presence of an AMD loader.
    // See http://requirejs.org/docs/errors.html#mismatch for more details.
    // Use `_.noConflict` to remove Lodash from the global object.
    root._ = _;// Define as an anonymous module so, through path mapping, it can be
    // referenced as the "underscore" module.
    define(function () { return _; });
  }// Check for `exports` after `define` in case a build optimizer adds it.
  else if (freeModule) {// Export for Node.js.
    (freeModule.exports = _)._ = _;// Export for CommonJS support.
    freeExports._ = _;
  } else {// Export to the global object.
    root._ = _;
  }

应该走到freeModule分支才对,于是找了找,发现webpack的一个运行机制。

  • Webpack 的“自作聪明” 在 Element-UI 内部打包的那份 Lodash 代码( lodash.js )中,有一个经典的 UMD 环境判断:
js
if(typeof define==='function'&&_typeof
(define.amd)==='object'&&define.amd){
    // ...
    root._=_; // <--- 罪魁祸首在这里
    define(function(){return _;});
}

当 Webpack(Vue CLI 底层)去打包这个文件时,它扫描到了 define.amd 。Webpack 为了兼容老代码,会默认提供一个模拟的 AMD 环境,在构建时自动把 define 注入进去,导致 define.amd 变成了 true 。

  • Lodash 走错了分支 因为 Webpack 注入了 AMD 变量,导致上面那个 if 判断在浏览器运行时成立了。Lodash 误以为自己正处于一个 AMD 加载器环境中,于是乖乖执行了 root._ = _ (这里的 root 就是 window ),从而把 _ 泄漏到了全局。

如果没有 Webpack 捣乱,它本该走到后面的 else if(freeModule) (CommonJS 分支),那样就绝对不会污染 window

后面我的最小用例可以验证这个 webpack-amd-demo

webpack解决全局变量暴露问题

js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  chainWebpack: config => {
    // 告诉 Webpack:遇到 lodash 不要去模拟 AMD 环境
    config.module
      .rule('disable-amd')
      .test(/lodash\.js$/)
      .parser({ amd: false })
  }
})

element-ui-ce为什么能解决

  • 位置lib/statistic.js, 524行中的引用

alt text

引用的是lodash-es,是treeshaking的版本,这就是根本原因

vite+element-ui+vue为什么没有

其实后面我又想了,vite构建工具应该不会有这样的问题把,最起码不会暴露出全局变量,试了一下果真如此。

alt text

因为在vite世界里,esm一等公民,在 Vite 的环境里,全局变量 define 就是 undefined ,正确走入 CommonJS 分支。 第二,tree-shaking,,能够明确推断出 typeof define === 'function' 永远是 false 。所以在最终你看到的 Vite 打包产物( dist/assets/index-xxx.js )里, 那段试图挂载到全局的 AMD 代码连同 root._ = _ 会被当作 Dead Code(死代码)直接删掉(Tree-shaking) 。

相关代码

element-ui

参考文章

上次更新于: