关于 file-uploads
面向处理 S3、Cloudflare R2 及相关上传流程的开发者技能。重点介绍大文件流式处理、预签名 URL,以及文件内容验证和访问控制等安全措施。
主要功能
- 01S3 与 Cloudflare R2 上传指导
- 02预签名 URL 模式
- 03分段及大文件处理
- 04文件内容与大小验证
- 05文件名和访问安全措施
适用场景
- 01设计直传存储流程
- 02处理大型文件传输
- 03验证上传的图像
- 04防止不安全文件名和访问
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill file-uploads安装后在 Claude Code 中输入/file-uploads确认已加载。
详细介绍
文件上传与存储
擅长处理文件上传和云存储。涵盖 S3、 Cloudflare R2、预签名 URL、分片上传和图像 优化。知道如何处理大文件而不阻塞。
角色:文件上传专家
注重安全性和性能。绝不信任文件 扩展名。知道大文件上传需要特殊处理。 优先使用预签名 URL,而不是通过服务器代理。
原则
- 绝不信任客户端对文件类型的声明
- 使用预签名 URL 直接上传
- 流式处理大文件,绝不缓冲
- 上传时验证,之后再优化
请将代码视为独立的 TypeScript 代码片段,并补上实际所需的 SDK 导入、存储客户端和处理器。魔数检测不等于恶意软件扫描,也不能证明图像安全:对获准格式进行解码和重新编码,并限制尺寸和资源用量;在验证完成前将文件隔离,并在适当情况下通过独立源站使用安全的内容响应头提供文件。对预签名请求的创建和对象访问都进行身份验证与授权,将租户、对象键和有效期绑定,并避免使用用户可控的存储路径。若要严格限制上传范围,请使用云服务商支持的签名策略,例如 S3 POST 的 content-length-range,或等效的强制机制;并在服务器端验证实际上传大小。ContentLength 并非跨服务商通用的大小限制。
易踩问题
信任客户端提供的文件类型
严重性:严重
情况:用户将 malware.exe 重命名为 image.jpg 上传。你检查 扩展名,看起来没问题。于是存储并提供该文件。另一位用户 下载并执行了它。
症状:
- 恶意软件伪装成图像上传
- 提供的内容类型错误
原因: 文件扩展名和 Content-Type 响应头都可以伪造。 攻击者会将可执行文件重命名,以绕过过滤器。
建议修复:
检查魔数
import { fileTypeFromBuffer } from "file-type";
async function validateImage(buffer: Buffer) { const type = await fileTypeFromBuffer(buffer);
const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
if (!type || !allowedTypes.includes(type.mime)) { throw new Error("Invalid file type"); }
return type; }
// 处理流 import { fileTypeFromStream } from "file-type"; const type = await fileTypeFromStream(readableStream);
未限制上传大小
严重性:高
情况:没有文件大小限制。攻击者上传 10GB 文件。服务器 内存或磁盘耗尽,导致拒绝服务。或者产生巨额 存储费用。
症状:
- 服务器在处理大文件上传时崩溃
- 存储费用暴涨
- 内存耗尽
原因: 没有限制时,攻击者可以耗尽资源。即使是 合法用户,也可能意外上传超大文件。
建议修复:
设置大小限制
// Formidable const form = formidable({ maxFileSize: 10 * 1024 * 1024, // 10MB });
// Multer const upload = multer({ limits: { fileSize: 10 * 1024 * 1024 }, });
// 客户端提前检查 if (file.size > 10 * 1024 * 1024) { alert("文件过大(最大 10MB)"); return; }
// 带大小限制的预签名 URL const command = new PutObjectCommand({ Bucket: BUCKET, Key: key, ContentLength: expectedSize, // 声明的大小;不是通用的上传大小策略 });
用户可控的文件名导致路径遍历
严重性:严重
情况:用户上传名为 "../../../etc/passwd" 的文件。你直接使用 文件名。文件被保存到上传目录之外。 系统文件被覆盖。
症状:
- 文件位于上传目录之外
- 可访问系统文件
原因: 绝不能在文件路径中直接使用用户输入。 路径遍历序列可能逃出预期目录。
建议修复:
清理文件名
import path from "path"; import crypto from "crypto";
function safeFilename(userFilename: string): string { // 仅提取基本名称 const base = path.basename(userFilename);
// 移除剩余的路径字符 const sanitized = base.replace(/[^a-zA-Z0-9.-]/g, "_");
// 或者更好的做法:完全生成新名称 const ext = path.extname(userFilename).toLowerCase(); const allowed = [".jpg", ".png", ".pdf"];
if (!allowed.includes(ext)) { throw new Error("Invalid extension"); }
return crypto.randomUUID() + ext; }
// 绝不要这样做 const path = "uploads/" + req.body.filename; // 危险!
// 应该这样做 const path = "uploads/" + safeFilename(req.body.filename);
预签名 URL 被错误共享或缓存
严重性:中
情况:API 响应中返回了私有文件的预签名 URL。 响应被 CDN 缓存。任何拿到缓存 URL 的人都能在数小时内访问 私有文件。
症状:
- 可通过缓存的 URL 访问私有文件
- URL 过期后仍可访问
问题原因: 预签名 URL 授予临时访问权限。如果被缓存或共享, 持有者 URL 可能在有效期内被非预期人员使用;即使 URL 过期,缓存的文件内容也可能继续留存。
建议修复方法:
控制预签名 URL 的分发
// 敏感文件使用较短的有效期 const url = await getSignedUrl(s3, command, { expiresIn: 300, // 5 分钟 });
// 为预签名 URL 响应设置禁止缓存的标头 return Response.json({ url }, { headers: { "Cache-Control": "no-store, max-age=0", }, });
// 或使用 CloudFront signed URLs,以获得更精细的控制
验证检查
只检查文件扩展名
严重性:严重
消息:检查文件魔数,而不只是扩展名
修复操作:使用 file-type 库验证实际类型
直接在路径中使用用户提供的文件名
严重性:严重
消息:清理文件名以防止路径遍历
修复操作:使用 path.basename() 并生成安全名称
协作
委派触发条件
- 图像优化 CDN -> performance-optimization(图像交付)
- 存储文件元数据 -> postgres-wizard(数据库架构)
适用场景
- 用户提及或暗示:文件上传
- 用户提及或暗示:S3
- 用户提及或暗示:R2
- 用户提及或暗示:预签名 URL
- 用户提及或暗示:分段上传
- 用户提及或暗示:图像上传
- 用户提及或暗示:云存储
示例
用户请求:
在此任务中使用 @file-uploads:擅长处理文件上传和云存储。
限制
- 仅当任务明确符合上述范围时,才使用此技能。
- 请勿将输出视为替代环境特定验证、测试或专家审查的方案。
- 如果缺少必要的输入、权限、安全边界或成功标准,请停止并要求澄清。