fix: file upload file name utf8 issues

This commit is contained in:
Tautvydas Derzinskas committed 2026-09-23 17:44:52 +02:00
1 parent b73299222a
commit 6d34243b89
3 files changed
+38 -3

No files matched your search

+11 -2
View File
@@ -12,8 +12,17 @@ const diskStorage = multer.diskStorage({
},
});
// Browsers send multipart filenames as raw UTF-8 bytes, but multer decodes them as Latin-1 by
// default -- `哨子.3mf` would arrive as mojibake, and sanitizeFilename then strips the C1 bytes
// needed to recover it.
const defParamCharset = "utf8";
/** Disk-backed multer instance for model file uploads (never memoryStorage — files can be large). */
export const modelUpload = multer({ storage: diskStorage, limits: { fileSize: IMPORT_MAX_BYTES } });
export const modelUpload = multer({ storage: diskStorage, limits: { fileSize: IMPORT_MAX_BYTES }, defParamCharset });
/** Small in-memory multer instance for the 8MB-capped client-rendered thumbnail upload. */
export const thumbnailUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 8 * 1024 * 1024 + 1 } });
export const thumbnailUpload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 8 * 1024 * 1024 + 1 },
defParamCharset,
});
+3 -1
View File
@@ -28,7 +28,9 @@ export function sanitizeFilename(name: string | null | undefined): string {
* alone instead of being mangled by the fix.
*/
function decodeLatin1AsUtf8(value: string): string {
if (!/[\u0080-\u00ff]/.test(value)) return value;
// Only a pure byte string can be one; anything above U+00FF was already decoded properly, and
// Buffer.from(..., "latin1") would silently truncate it.
if (!/[\u0080-\u00ff]/.test(value) || /[\u0100-\uffff]/.test(value)) return value;
const bytes = Buffer.from(value, "latin1");
const decoded = bytes.toString("utf8");
return Buffer.from(decoded, "utf8").equals(bytes) ? decoded : value;
+24
View File
@@ -1,4 +1,7 @@
import express from "express";
import request from "supertest";
import { describe, expect, it } from "vitest";
import { modelUpload } from "../src/uploadMiddleware";
import { buildImportFilename, parseContentDisposition, sanitizeFilename } from "../src/utils/fileUtils";
// HTTP header values are ISO-8859-1 (RFC 7230 section 3.2.4). A server that puts raw UTF-8 bytes
@@ -59,3 +62,24 @@ describe("buildImportFilename", () => {
expect(/[\u0000-\u001f\u007f-\u009f]/.test(name)).toBe(false);
});
});
describe("parseContentDisposition with already-decoded input", () => {
it("leaves a string with characters above U+00FF alone", () => {
// Not a byte string, so there is nothing to re-decode -- and latin1 would truncate `哨`.
expect(parseContentDisposition('attachment; filename="café哨.stl"')).toBe("café哨.stl");
});
});
describe("modelUpload", () => {
it("decodes a browser's raw UTF-8 multipart filename as UTF-8, not Latin-1", async () => {
const app = express();
app.post("/upload", modelUpload.single("file"), (req, res) => {
res.json({ name: sanitizeFilename(req.file?.originalname) });
});
const res = await request(app)
.post("/upload")
.attach("file", Buffer.from("solid x\nendsolid x\n"), { filename: "哨子.stl", contentType: "model/stl" });
expect(res.status).toBe(200);
expect(res.body.name).toBe("哨子.stl");
});
});