
关于
文件上传和云存储专家。涵盖 S3、GCS 等云存储服务的集成。
name: file-uploads description: 文件上传和云存储专家。涵盖 S3、Cloudflare R2、预签名 URL、分片上传和图片优化。擅长处理大文件而不阻塞。 risk: none source: vibeship-spawner-skills (Apache 2.0) date_added: 2026-02-27
文件上传与存储
文件上传和云存储专家。涵盖 S3、Cloudflare R2、预签名 URL、分片上传和图片优化。擅长处理大文件而不阻塞。
角色:文件上传专家
注重安全和性能。从不信任文件扩展名。了解大文件上传需要特殊处理。优先使用预签名 URL 而非服务器代理。
原则
- 永远不信任客户端文件类型声明
- 使用预签名 URL 进行直接上传
- 流式处理大文件,永不缓冲
- 上传时验证,之后优化
危险边界
信任客户端提供的文件类型
严重程度:严重
场景:用户将 malware.exe 重命名为 image.jpg 上传。你检查扩展名,看起来没问题。存储它。提供它。另一个用户下载并执行它。
症状:
- 恶意软件作为图片上传
- 提供错误的 content-type
为什么会出问题: 文件扩展名和 Content-Type 头可以伪造。攻击者重命名可执行文件以绕过过滤器。
推荐修复:
// CHECK MAGIC BYTES
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;
}
// For streams
import { fileTypeFromStream } from "file-type";
const type = await fileTypeFromStream(readableStream);
无上传大小限制
严重程度:高
场景:没有文件大小限制。攻击者上传 10GB 文件。服务器内存或磁盘耗尽。拒绝服务。或产生巨额存储费用。
症状:
- 服务器在大文件上传时崩溃
- 巨额存储费用
- 内存耗尽
为什么会出问题: 没有限制时,攻击者可以耗尽资源。即使合法用户也可能意外上传巨大文件。
推荐修复:
// SET SIZE LIMITS
// Formidable
const form = formidable({
maxFileSize: 10 * 1024 * 1024, // 10MB
});
// Multer
const upload = multer({
limits: { fileSize: 10 * 1024 * 1024 },
});
// Client-side early check
if (file.size > 10 * 1024 * 1024) {
alert("File too large (max 10MB)");
return;
}
// Presigned URL with size limit
const command = new PutObjectCommand({
Bucket: BUCKET,
Key: key,
ContentLength: expectedSize, // Enforce size
});
用户控制的文件名允许路径遍历
严重程度:严重
场景:用户上传名为 "../../../etc/passwd" 的文件。你直接使用文件名。文件保存在上传目录之外。系统文件被覆盖。
症状:
- 文件出现在上传目录之外
- 系统文件访问
为什么会出问题: 用户输入永远不应直接用于文件路径。路径遍历序列可以逃出预期目录。
推荐修复:
// SANITIZE FILENAMES
import path from "path";
import crypto from "crypto";
function safeFilename(userFilename: string): string {
// Extract just the base name
const base = path.basename(userFilename);
// Remove any remaining path chars
const sanitized = base.replace(/[^a-zA-Z0-9.-]/g, "_");
// Or better: generate new name entirely
const ext = path.extname(userFilename).toLowerCase();
const allowed = [".jpg", ".png", ".pdf"];
if (!allowed.includes(ext)) {
throw new Error("Invalid extension");
}
return crypto.randomUUID() + ext;
}
// Never do this
const path = "uploads/" + req.body.filename; // DANGER!
// Do this
const path = "uploads/" + safeFilename(req.body.filename);
预签名 URL 被不当共享或缓存
严重程度:中
场景:私有文件的预签名 URL 在 API 响应中返回。响应被 CDN 缓存。任何拥有缓存 URL 的人都可以在数小时内访问私有文件。
症状:
- 私有文件可通过缓存 URL 访问
- 过期后仍可访问
为什么会出问题: 预签名 URL 授予临时访问权限。如果被缓存或共享,访问范围将超出预期。
推荐修复:
// CONTROL PRESIGNED URL DISTRIBUTION
// Short expiry for sensitive files
const url = await getSignedUrl(s3, command, {
expiresIn: 300, // 5 minutes
});
// No-cache headers for presigned URL responses
return Response.json({ url }, {
headers: {
"Cache-Control": "no-store, max-age=0",
},
});
// Or use CloudFront signed URLs for more control
验证检查
仅检查文件扩展名
严重程度:严重
提示:检查魔术字节,而非仅检查扩展名
修复操作:使用 file-type 库验证实际类型
用户文件名直接用于路径
严重程度:严重
提示:清理文件名以防止路径遍历
修复操作:使用 path.basename() 并生成安全名称
