近日,部分 Node.js 开发者在使用 Express 框架配合 Formidable 模块处理文件上传时,遭遇一个令人困惑的 Bug:上传的文件无论其原始名称是否合法,都会被重命名为字符串 "invalid-name"。这一问题在社区引发广泛讨论,技术团队正在排查根因。

问题重现:从“文件名有效”到“强制更名”

一位来自上海的 Node.js 全栈工程师在技术博客中详细描述了该现象。他使用 Express 搭建了一个简单的文件上传服务,并引入 Formidable 模块解析 multipart/form-data 数据。代码逻辑如下:

const express = require('express');
const formidable = require('formidable');

const app = express();

app.post('/upload', (req, res) => {
  const form = new formidable.IncomingForm();
  form.parse(req, (err, fields, files) => {
    if (err) {
      return res.status(400).send('Upload error');
    }
    console.log(files.uploadFile.originalFilename); // 预期是 'test.txt'
    // 实际输出:'invalid-name'
  });
});

经过多次测试,无论上传的文件名包含中文、英文、数字还是特殊字符(如空格、下划线、连字符),files.uploadFile.originalFilename 属性始终返回字符串 "invalid-name"。该工程师表示:“我甚至尝试了最简单的文件名 a.txt,结果依然是 invalid-name——这完全颠覆了对‘有效文件名’的认知。”

技术溯源:问题出在文件名验证环节

Formidable 是一个广受使用的 Node.js 文件上传解析库,其内部对上传文件名的处理遵循以下流程:从 multipart 数据中提取 Content-Disposition 头中的 filename 参数,然后通过内部函数 isValidFileName 进行校验。如果校验不通过,则将该字段替换为 "invalid-name"

深入阅读 Formidable 源码后发现,isValidFileName 函数在版本 3.5.1 及更高版本中进行了重构。它采用白名单策略,仅允许文件名匹配正则 /^[a-zA-Z0-9._-]+$/ ——也就是说,文件名 只能包含英文字母、数字、点、下划线和短横线,且必须由这些字符组成,不得包含空格、中文、扩展拉丁字符等。如果一个文件名包含任何不在白名单中的字符(例如空格、括号、汉字、emoji),就会被判定为“无效”,进而被强制覆盖为 "invalid-name"

更值得关注的是,Formidable3.5.0 版本 时并未启用这一严格校验,而是采用了更宽松的路径安全检查。从 3.5.1 开始,默认行为被收紧,导致大量此前“有效”的文件名如今被拦截。许多开发者直接使用 npm install formidable 安装最新版,无意中触发了这一突变。

影响范围:旧项目升级需谨慎

该问题主要影响以下几类用户:

  • 从 Formidable 2.x 或 3.5.0 升级到 3.5.1+ 的项目:升级后上传行为突然变化,旧文件无法保持原名。
  • 需要支持国际化文件名的系统:例如用户上传中文名称的合同、日文图片等,均会被重命名。
  • 依赖 originalFilename 进行后续业务处理的场景:如按原文件名存储、关联数据库记录等,逻辑会瞬间崩溃。

一位来自北京某电商平台的运维工程师在社区吐槽:“我们的用户上传发票时,文件名通常包含中文如‘2024年01月发票.pdf’,升级后全部变成‘invalid-name.pdf’。不仅数据库里的记录变得不可读,连发票识别系统都报错了。”

官方与社区反应:紧急修复与临时规避

在 GitHub 仓库的 Issue 区,该问题已被标记为 “bug(行为变更)。社区开发者批评此次更新未通过 major 版本号警告兼容性破坏,且默认启用过于严格的校验而未提供明确文档。截至发稿,维护者已发布 3.5.2 版本,将默认校验规则回退至更宽松的版本,同时保留 options.filename 自定义验证接口。用户可通过以下方式临时规避:

  1. 降级npm install formidable@3.5.0(回退到旧版行为)。
  2. 自定义验证:在创建 IncomingForm 实例时传入 filename 选项,覆盖默认规则:
const form = new formidable.IncomingForm({
  filename: (name, ext, part, form) => {
    // 返回原始名称,不做校验
    return name + ext;
  }
});
  1. 升级至 3.5.2+:该版本已将白名单扩展为包含空格、常见标点及 Unicode 字符,但仍建议业务侧自行验证文件名安全性。

行业建议:关注依赖库的语义化版本

此次事件也再次提醒开发者,在升级第三方库时应严格遵循语义化版本(SemVer)规范。minor 版本更新不应引入破坏性改变,更不应在未通知的情况下修改核心功能的行为。对于涉及用户输入(如文件名)的模块,建议:

  • 在开发环境中搭建自动化测试,覆盖不同语言、特殊字符的文件名。
  • 使用 package-lock.jsonyarn.lock 锁定版本,避免“无意识升级”带来的风险。
  • 对于文件上传业务,始终在服务器端进行二次文件名校验与净化,不依赖第三方库的默认处理。

结语

Formidable"invalid-name" 问题虽已在最新版本中修复,但它暴露了现代 Node.js 生态中一个贯穿始终的矛盾:库的“默认安全”倾向与用户业务灵活性的冲突。安全与便利之间的平衡艺术,始终需要维护者与使用者共同思考。对于目前仍受影响的项目,采用上述规避方案即可快速恢复业务;而对于所有依赖文件上传的开发者,这一事件也提醒我们:永远不要假设第三方库对“有效文件名”的定义与你的业务场景一致。