mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: truncate long filenames to prevent filesystem ENAMETOOLONG errors
This commit is contained in:
@@ -45,5 +45,18 @@ export function sanitizeFilename(raw: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Truncate to filesystem-safe length (255 byte NAME_MAX minus margin for _toolId suffix)
|
||||||
|
const MAX_NAME_BYTES = 200;
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
if (enc.encode(name).length > MAX_NAME_BYTES) {
|
||||||
|
const dotIdx = name.lastIndexOf(".");
|
||||||
|
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
|
||||||
|
let base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
||||||
|
while (enc.encode(base + ext).length > MAX_NAME_BYTES) {
|
||||||
|
base = base.slice(0, -1);
|
||||||
|
}
|
||||||
|
name = base + ext;
|
||||||
|
}
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -821,6 +821,46 @@ function sanitizeFilename(raw: string): string {
|
|||||||
if (!name || name === "." || name === "..") {
|
if (!name || name === "." || name === "..") {
|
||||||
name = "upload";
|
name = "upload";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard against double-extension attacks (e.g. "image.png.php").
|
||||||
|
const dotIndex = name.indexOf(".");
|
||||||
|
if (dotIndex !== -1) {
|
||||||
|
const parts = name.split(".");
|
||||||
|
const SAFE_IMAGE_EXTENSIONS = new Set([
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".png",
|
||||||
|
".webp",
|
||||||
|
".gif",
|
||||||
|
".bmp",
|
||||||
|
".tiff",
|
||||||
|
".tif",
|
||||||
|
".avif",
|
||||||
|
".svg",
|
||||||
|
".pdf",
|
||||||
|
]);
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
const ext = `.${parts[i].toLowerCase()}`;
|
||||||
|
if (SAFE_IMAGE_EXTENSIONS.has(ext)) {
|
||||||
|
name = parts.slice(0, i + 1).join(".");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate to filesystem-safe length (255 byte NAME_MAX minus margin for _toolId suffix)
|
||||||
|
const MAX_NAME_BYTES = 200;
|
||||||
|
const enc = new TextEncoder();
|
||||||
|
if (enc.encode(name).length > MAX_NAME_BYTES) {
|
||||||
|
const dotIdx = name.lastIndexOf(".");
|
||||||
|
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
|
||||||
|
let base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
|
||||||
|
while (enc.encode(base + ext).length > MAX_NAME_BYTES) {
|
||||||
|
base = base.slice(0, -1);
|
||||||
|
}
|
||||||
|
name = base + ext;
|
||||||
|
}
|
||||||
|
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -961,9 +1001,18 @@ describe("sanitizeFilename", () => {
|
|||||||
expect(sanitizeFilename("my..file..name.png")).toBe("myfilename.png");
|
expect(sanitizeFilename("my..file..name.png")).toBe("myfilename.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles very long filenames", () => {
|
it("handles very long filenames by truncating to filesystem-safe length", () => {
|
||||||
const longName = "a".repeat(500) + ".png";
|
const longName = "a".repeat(500) + ".png";
|
||||||
expect(sanitizeFilename(longName)).toBe(longName);
|
const result = sanitizeFilename(longName);
|
||||||
|
expect(new TextEncoder().encode(result).length).toBeLessThanOrEqual(200);
|
||||||
|
expect(result).toMatch(/\.png$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("truncates very long filenames to filesystem-safe length", () => {
|
||||||
|
const longName = "a".repeat(300) + ".jpg";
|
||||||
|
const result = sanitizeFilename(longName);
|
||||||
|
expect(new TextEncoder().encode(result).length).toBeLessThanOrEqual(200);
|
||||||
|
expect(result).toMatch(/\.jpg$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles filename with only extension", () => {
|
it("handles filename with only extension", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user