← 中文资源

简体中文版本

上传文件名安全清洗

去除路径、控制字符、保留设备名与危险字符,并限制长度,生成可安全落盘的上传文件名。

阅读英文原文

功能说明

sanitizeFilename 把用户上传的原始文件名转换为可安全存储的名称,去掉目录部分、控制字符、Windows 保留字符与首尾点号,避开 CON、NUL、COM1 等保留设备名,并限制总长度。它适合上传接口、对象存储键生成和批量导入。返回的只是文件名,不包含目录,调用方应自行拼接受控目录。

关键代码

输入先做 NFC 归一化,避免同形不同编码的文件名绕过去重。按正反斜杠取最后一段,替换非法字符与空白,去除首尾点和空格。扩展名最多保留十六个字符,主体按最大长度减去扩展名后的预算截断。若主体命中保留名则加下划线前缀。所有路径分隔符不会出现在结果中,因此不能借文件名穿越目录。

使用注意

清洗不能替代扩展名白名单与内容类型检测,攻击者仍可上传可执行脚本或双扩展名文件,落盘后应禁用执行权限并通过独立域名提供下载。保留名规则针对 Windows,跨平台部署仍需统一策略。Unicode 归一化可能改变原文件名,若需保留展示名应另存元数据字段。截断可能造成同名冲突,应追加随机标识。

验证方式

测试 ../../etc/passwd、C:\temp\file.txt、带空字节、控制字符、冒号与问号的文件名,确认路径分隔符消失。测试 CON、nul.txt、COM1.log 会被加前缀。验证连续空格折叠、首尾点去除、空文件名回退、超长文件名截断以及扩展名保留。最后测试非法 replacement、零长度上限与非字符串输入的处理。

代码示例

const RESERVED_NAMES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;

export interface SanitizeOptions {
  maxLength?: number;
  replacement?: string;
  fallback?: string;
}

export function sanitizeFilename(input: string, options: SanitizeOptions = {}): string {
  const { maxLength = 120, replacement = "_", fallback = "file" } = options;
  if (!Number.isInteger(maxLength) || maxLength < 1) {
    throw new RangeError("maxLength must be a positive integer");
  }
  if (!replacement || /[\\/:*?"<>|]/.test(replacement)) {
    throw new TypeError("replacement contains forbidden characters");
  }
  const normalized = String(input).normalize("NFC").replace(/[\u0000-\u001f\u007f]/g, "");
  const base = normalized.split(/[\\/]/).pop() ?? "";
  let name = base
    .replace(/[<>:"|?*]/g, replacement)
    .replace(/\s+/g, " ")
    .trim()
    .replace(/^\.+/, "")
    .replace(/\.+$/, "");
  if (!name) return fallback;
  const dot = name.lastIndexOf(".");
  const extension = dot > 0 ? name.slice(dot).slice(0, 16) : "";
  let stem = dot > 0 ? name.slice(0, dot) : name;
  if (RESERVED_NAMES.test(stem)) stem = "_" + stem;
  const budget = Math.max(1, maxLength - extension.length);
  stem = stem.slice(0, budget).trim();
  if (!stem) stem = fallback;
  name = stem + extension;
  return name;
}