mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
63 lines
1.7 KiB
TypeScript
63 lines
1.7 KiB
TypeScript
import { basename } from "node:path";
|
|
|
|
const SAFE_IMAGE_EXTENSIONS = new Set([
|
|
".jpg",
|
|
".jpeg",
|
|
".png",
|
|
".webp",
|
|
".gif",
|
|
".bmp",
|
|
".tiff",
|
|
".tif",
|
|
".avif",
|
|
".svg",
|
|
".pdf",
|
|
]);
|
|
|
|
/**
|
|
* Sanitize a filename to prevent path traversal and double-extension attacks.
|
|
*
|
|
* 1. Strips directory separators (basename only).
|
|
* 2. Removes ".." sequences and null bytes.
|
|
* 3. Truncates after the first recognised image extension so that
|
|
* "photo.png.php" becomes "photo.png".
|
|
*/
|
|
export function sanitizeFilename(raw: string): string {
|
|
let name = basename(raw);
|
|
name = name.replace(/\.\./g, "");
|
|
name = name.replace(/\0/g, "");
|
|
if (!name || name === "." || name === "..") {
|
|
name = "upload";
|
|
}
|
|
|
|
// Guard against double-extension attacks (e.g. "image.png.php").
|
|
// Walk the dot-separated parts and truncate after the first safe image extension.
|
|
const dotIndex = name.indexOf(".");
|
|
if (dotIndex !== -1) {
|
|
const parts = name.split(".");
|
|
for (let i = 1; i < parts.length; i++) {
|
|
const ext = `.${parts[i].toLowerCase()}`;
|
|
if (SAFE_IMAGE_EXTENSIONS.has(ext)) {
|
|
// Keep everything up to and including this extension, drop the rest
|
|
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;
|
|
}
|