fix: resolve 14 security, correctness, and robustness issues found during QA sweep

Security fixes:
- Add auth + ownership check to thumbnail endpoint (was unauthenticated)
- Validate ExifTool fieldsToRemove against safe tag name pattern
- Add SVG sanitization to pipeline execute and batch endpoints
- Replace basename() with sanitizeFilename() in 16 tool routes
- Escape SQL LIKE wildcards in file search to prevent pattern injection
- Improve settings HTML tag validation pattern

Bug fixes:
- Skip autoOrient for SVG inputs in pipeline (prevents misinterpretation)
- Remove double-encode in compress targetSize (was degrading quality)
- Fix bg-effects alpha value from 255 to 1.0 (Sharp expects float)
- Guard download stream error handler against headers-already-sent race
- Use O_EXCL atomic file creation for install lock (fixes TOCTOU race)
- Truncate collage file array to template image count

UX fixes:
- Accept empty JSON bodies on POST endpoints (install/uninstall)
- Custom JSON content type parser that treats empty body as {}
This commit is contained in:
SnapOtter
2026-05-01 18:11:49 +08:00
parent 1a2288f99b
commit ff8dcf63c7
27 changed files with 107 additions and 61 deletions
+3 -3
View File
@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto";
import { basename, extname } from "node:path";
import archiver from "archiver";
import type { FastifyInstance } from "fastify";
import sharp from "sharp";
@@ -7,6 +6,7 @@ import { z } from "zod";
import { autoOrient } from "../../lib/auto-orient.js";
import { formatZodErrors } from "../../lib/errors.js";
import { validateImageBuffer } from "../../lib/file-validation.js";
import { sanitizeFilename } from "../../lib/filename.js";
import { ensureSharpCompat } from "../../lib/heic-converter.js";
const settingsSchema = z.object({}).passthrough();
@@ -39,7 +39,7 @@ export function registerFavicon(app: FastifyInstance) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);
const filename = basename(part.filename ?? `image-${uploadedFiles.length + 1}`);
const filename = sanitizeFilename(part.filename ?? `image-${uploadedFiles.length + 1}`);
uploadedFiles.push({ buffer, filename });
} else if (part.fieldname === "settings") {
settingsRaw = part.value as string;
@@ -97,7 +97,7 @@ export function registerFavicon(app: FastifyInstance) {
for (const file of uploadedFiles) {
// Decode HEIC/HEIF if needed, then normalize EXIF orientation
const decoded = await autoOrient(await ensureSharpCompat(file.buffer));
const stem = basename(file.filename, extname(file.filename));
const stem = sanitizeFilename(file.filename).replace(/\.[^.]+$/, "");
// Single file: flat structure. Multiple files: per-image folders.
const prefix = isSingleFile ? "" : `${stem}/`;