Webpack 5 发布后,Module Federation 让多个独立构建的前端应用可以共享代码和依赖,但这也打开了新的攻击面:一个被入侵的远程模块可能在不经过完整源码审计的情况下进入宿主应用,执行任意代码或篡改运行时状态。这类攻击被社区称为 Model Poisoning(模型投毒)。这里的“模型”可以理解为 Webpack 构建产物中记录的模块依赖模型,一旦该模型被恶意修改,整个应用的行为就可能被无声无息地改变。本文将深入介绍这一新特性及其防护机制。

模型投毒在 Webpack 5 中的具体表现
在 Webpack 的编译结果中,每个模块都有一个唯一的 ID 和导出映射。当你在 Webpack 配置里使用 Module Federation 暴露或引用远程模块时,构建工具会生成一份 manifest 文件,记录远程模块的入口、共享依赖和版本信息。如果攻击者替换了远程入口文件,或者在远程 chunk 中注入了修改全局对象、原型链的代码,而宿主的 manifest 校验不够严格,就会发生模型投毒。
典型的表现包括:远程模块的默认导出被替换成另一个函数;共享依赖对象被添加了额外的属性;或者 chunk 加载阶段执行了非预期的副作用操作。由于 Webpack 运行时基于模块缓存机制,一旦被污染的模块被加载并缓存,后续所有引用该模块的代码都会受到影响。因此,模型投毒不只是简单的 XSS,它可能破坏整个共享模块体系的完整性。
为了理解防护原理,需要先了解 Webpack 5 中 moduleGraph 的作用。在编译阶段,compilation.moduleGraph 保存了模块之间的依赖关系、导出信息以及模块来源。开发者可以通过插件访问该图,分析远程模块是否有异常的导入或导出行为。这为模型投毒检测提供了基础数据。
如何通过插件检测模型投毒风险
Webpack 5 没有提供一个开箱即用的“模型投毒检测器”,但它的插件 API 足够强大,可以让我们在构建阶段和运行时嵌入检测逻辑。下面先介绍构建期的检测方法。我们可以编写一个自定义 Webpack 插件,遍历模块图,筛选出来自远程容器的模块,并检查其源码是否符合某些危险特征,例如包含 eval 调用、Object.prototype 赋值,或对全局对象 window 进行扩展。
// webpack-plugin-model-poisoning.js
class ModelPoisoningDetectPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap('ModelPoisoningDetectPlugin', (compilation) => {
compilation.hooks.finishModules.tap('ModelPoisoningDetectPlugin', (modules) => {
for (const module of modules) {
// 只检查远程模块,这里通过 module.layer 或自定义标识判断
if (module.layer !== 'remote') continue;
const source = module.originalSource();
if (!source) continue;
const code = source.source().toString();
const dangerousPatterns = [
/\beval\s*\(/,
/Object\.prototype\s*\[/,
/window\.\w+\s*=\s*function/,
/document\.write\s*\(/
];
const matched = dangerousPatterns.filter((pattern) => pattern.test(code));
if (matched.length > 0) {
const warning = new Error(`Potential model poisoning detected in module ${module.identifier()} with patterns: ${matched.map((p) => p.toString()).join(', ')}`);
compilation.warnings.push(warning);
}
}
});
});
}
}
module.exports = ModelPoisoningDetectPlugin;
这段代码展示了构建期静态检测的基本思路。但需要注意的是,攻击者往往会混淆代码,静态匹配只能作为第一道防线。更可靠的方案是在运行时对远程模块的实际导出做签名校验。Webpack 5 的 Module Federation 在加载远程模块时会调用 __webpack_require__.l 等内部函数,我们可以在远程模块加载完成后,验证其导出对象的结构是否与构建时生成的结构一致。
除了插件,Webpack 5 的 experiments.federation 配置也提供了 shared 和 remotes 的版本锁定能力。通过为每个远程模块指定精确的版本号或哈希值,可以减少被替换的风险。不过版本号本身也可能被伪造,因此需要在远程服务器端启用完整性校验,例如使用 SRI(Subresource Integrity)或对 chunk 文件进行签名。
运行时隔离与白名单控制的最佳实践
模型投毒的核心危害在于远程模块可以不受限制地访问宿主应用的全局对象。因此,运行时隔离是最有效的防护手段之一。在 Webpack 5 中,可以通过 output.library.type 设置为 module 或 system,让远程模块运行在更严格的作用域中。同时,利用 __webpack_require__.C 提供的模块加载钩子,可以在加载远程模块前注入一个代理环境,限制其对 window、document 的访问。
// 在远程模块加载前注入运行时防护
const remoteEntryUrl = 'http://remote.ippipp.com/remoteEntry.js';
const loadRemote = (url, scope, module) => {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = () => {
const container = window[scope];
if (!container) {
reject(new Error('Remote container not found'));
return;
}
// 白名单检查:导出名称必须匹配预期列表
const allowedExports = ['getUserInfo', 'fetchOrderList'];
const actualExports = Object.keys(container.init(new Proxy({}, {
get(target, prop) {
if (prop === 'react') return window.react; // 共享依赖白名单
throw new Error(`Blocked access to global ${prop}`);
}
})) || {});
if (!allowedExports.every((name) => actualExports.includes(name))) {
reject(new Error('Remote module exports do not match allowed list'));
return;
}
resolve(container.get(module));
};
script.onerror = reject;
document.head.appendChild(script);
});
};
上述代码使用 Proxy 创建了一个受限的共享作用域,当远程模块尝试访问未在白名单内的全局变量时,会直接抛出异常。同时,在加载完成后对比实际导出列表与预期列表,防止导出对象被替换。这种运行时防护可以有效阻止大部分模型投毒攻击,因为恶意代码很难在受限环境中执行。
生产环境中还应结合 Content Security Policy(CSP)限制远程脚本的来源,并定期对远程模块的构建产物做哈希比对。如果使用 CI/CD 流程,可以在构建完成后发布模块签名文件,宿主在运行前通过独立的 API 获取签名并验证。这些措施与 Webpack 5 的模型投毒防护理念一致:不信任任何远程输入,始终校验来源和内容完整性。
需要明确的是,模型投毒防护并不是 Webpack 5 的一个独立开关,而是由 Module Federation 的版本锁定能力、自定义构建插件、运行时隔离以及部署策略共同组成的工程方案。对于已经使用微前端架构的团队,尽早将这类检查集成到构建和发布流程中,可以显著降低被投毒模块影响整个应用的风险。