ValidateAntiForgeryToken对文件上传无效,因其仅校验表单字段,而multipart/form-data中文件流绕过模型绑定,且静态令牌易被重放;须改用服务端签发的一次性限时凭证(如JWT),绑定用户、时间与salt,上传前获取、入口处验证、通过后立即作废并防重放。

为什么 ValidateAntiForgeryToken 对文件上传无效
CSRF 防护令牌(ValidateAntiForgeryToken)默认只检查表单字段,而 multipart/form-data 请求中文件流本身不经过模型绑定器校验,令牌若仅放在 HTML 表单 hidden 字段里,攻击者完全可以复制整个请求(含有效令牌)重放——只要令牌还没过期,服务端就认它合法。
- 文件上传接口常被设计为「无状态」或「快速响应」,容易忽略会话上下文校验
- 前端 JS 用
FormData.append()构造请求时,可能无意中把旧的__RequestVerificationToken带进新请求 - 后端若用
IFormFile直接读取而没校验请求来源时效性,等于给重放开了绿灯
必须在服务端生成一次性上传凭证
不能依赖客户端传来的任何静态令牌,得让每次上传请求都携带服务端签发、限时有效的凭证。最简方式是:前端发起上传前,先调一次 /api/upload/token 获取 JWT 或加密字符串,再把它作为额外字段(如 x-upload-token header 或 _token 表单字段)随文件一起提交。
- 凭证需绑定用户 ID + 时间戳 + 随机 salt,用
HMACSHA256或Microsoft.IdentityModel.Tokens签发,有效期建议 ≤5 分钟 - 验证逻辑必须在
HttpPostaction 入口处做,且早于Request.Form.Files读取——否则攻击者可能已触发文件写入 - 验证失败直接返回
400 Bad Request,不要抛异常,避免暴露内部逻辑
// 示例:签发 token(服务端)
var token = new JwtSecurityToken(
issuer: "upload",
audience: userId,
notBefore: DateTime.UtcNow,
expires: DateTime.UtcNow.AddMinutes(5),
signingCredentials: credentials);
上传后立即作废凭证并检查重复指纹
光验证 token 有效性不够,得防止同一凭证被多次使用。关键是在验证通过后、保存文件前,把该 token 的哈希值存入分布式缓存(如 Redis),设置和 token 相同的过期时间;下次收到相同 token 就直接拒掉。
- 指纹推荐用
Convert.ToBase64String(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken))),避免明文存储 - 如果业务允许,可额外计算文件内容 SHA256(限小文件),连同 token 一起查重——能防「换文件但复用 token」的绕过
- 注意:Redis key 过期策略必须是
EXPIRE,不能靠应用层清理,否则集群下易失效
前端必须禁用重复点击且清空 token 缓存
按钮点击后立刻置灰、移除事件监听,比后端防护更前置。但很多人忘了:JS 里存的 token 是内存变量,页面没刷新时,用户点「返回」再进来,可能还拿着旧 token。
- 上传成功后调用
sessionStorage.removeItem("upload_token"),而不是只清空 DOM 中的 hidden 字段 - 如果用 axios,务必在
interceptors.request里动态读取 token,别在 config 里写死 - 不要用
setTimeout模拟“防抖”,用户关掉页面再重开,照样能重放——必须依赖服务端凭证生命周期
真正难的不是加一层校验,而是让前后端对「一次上传 = 一个不可再生凭证 + 一次原子化校验+作废」形成严格共识。漏掉任意一环,重放就可能穿透。










