mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -84,6 +84,16 @@ const app = Fastify({
|
||||
routerOptions: { maxParamLength: 500 },
|
||||
});
|
||||
|
||||
app.removeContentTypeParser("application/json");
|
||||
app.addContentTypeParser("application/json", { parseAs: "string" }, (_request, body, done) => {
|
||||
try {
|
||||
const str = typeof body === "string" ? body : (body as Buffer).toString();
|
||||
done(null, str.length > 0 ? JSON.parse(str) : {});
|
||||
} catch (err) {
|
||||
done(err as Error, undefined);
|
||||
}
|
||||
});
|
||||
|
||||
app.setErrorHandler((error: Error & { statusCode?: number }, request, reply) => {
|
||||
const statusCode = error.statusCode ?? 500;
|
||||
request.log.error(
|
||||
|
||||
@@ -132,7 +132,7 @@ export async function compositeOnColor(subjectBuffer: Buffer, hexColor: string):
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
channels: 4,
|
||||
background: { r, g, b, alpha: 255 },
|
||||
background: { r, g, b, alpha: 1 },
|
||||
},
|
||||
})
|
||||
.composite([{ input: subjectBuffer, blend: "over" }])
|
||||
@@ -223,7 +223,7 @@ export async function applyEffects(
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
background = await sharp({
|
||||
create: { width, height, channels: 4, background: { r, g, b, alpha: 255 } },
|
||||
create: { width, height, channels: 4, background: { r, g, b, alpha: 1 } },
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
@@ -234,10 +234,12 @@ export function buildTagArgs(settings: EditMetadataSettings): string[] {
|
||||
if (settings.iptcState) args.push(`-IPTC:Province-State=${settings.iptcState}`);
|
||||
if (settings.iptcCountry) args.push(`-IPTC:Country-PrimaryLocationName=${settings.iptcCountry}`);
|
||||
|
||||
// Field removal
|
||||
// Field removal -- only allow safe EXIF/IPTC/XMP tag names (alphanumeric, colon, hyphen)
|
||||
if (settings.fieldsToRemove && settings.fieldsToRemove.length > 0) {
|
||||
for (const field of settings.fieldsToRemove) {
|
||||
args.push(`-${field}=`);
|
||||
if (/^[A-Za-z0-9:_-]+$/.test(field)) {
|
||||
args.push(`-${field}=`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
constants,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
@@ -133,16 +135,17 @@ interface LockData {
|
||||
}
|
||||
|
||||
export function acquireInstallLock(bundleId: string): boolean {
|
||||
if (existsSync(LOCK_PATH)) {
|
||||
try {
|
||||
const fd = openSync(LOCK_PATH, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
|
||||
const lock: LockData = {
|
||||
bundleId,
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
writeFileSync(fd, JSON.stringify(lock, null, 2), "utf-8");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lock: LockData = {
|
||||
bundleId,
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
writeFileSync(LOCK_PATH, JSON.stringify(lock, null, 2), "utf-8");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function releaseInstallLock(): void {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { validateImageBuffer } from "../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../lib/heic-converter.js";
|
||||
import { isSvgBuffer, sanitizeSvg } from "../lib/svg-sanitize.js";
|
||||
import { createWorkspace } from "../lib/workspace.js";
|
||||
import { hasEffectivePermission } from "../permissions.js";
|
||||
import { requireAuth } from "../plugins/auth.js";
|
||||
@@ -138,8 +139,13 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize EXIF orientation before passing to pipeline steps
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
// Sanitize SVG input and normalize EXIF orientation
|
||||
const isSvg = isSvgBuffer(fileBuffer);
|
||||
if (isSvg) {
|
||||
fileBuffer = sanitizeSvg(fileBuffer);
|
||||
} else {
|
||||
fileBuffer = await autoOrient(fileBuffer);
|
||||
}
|
||||
|
||||
// Parse and validate the pipeline definition
|
||||
if (!pipelineRaw) {
|
||||
@@ -582,8 +588,12 @@ export async function registerPipelineRoutes(app: FastifyInstance): Promise<void
|
||||
if (ext) currentFilename = `${currentFilename.slice(0, -ext.length)}.png`;
|
||||
}
|
||||
|
||||
// Normalize EXIF orientation
|
||||
currentBuffer = await autoOrient(currentBuffer);
|
||||
// Sanitize SVG or normalize EXIF orientation
|
||||
if (isSvgBuffer(currentBuffer)) {
|
||||
currentBuffer = sanitizeSvg(currentBuffer);
|
||||
} else {
|
||||
currentBuffer = await autoOrient(currentBuffer);
|
||||
}
|
||||
|
||||
// Run through all pipeline steps sequentially
|
||||
for (let i = 0; i < pipeline.steps.length; i++) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { requireAuth } from "../plugins/auth.js";
|
||||
|
||||
const settingsBodySchema = z.record(z.string().min(1), z.unknown());
|
||||
|
||||
const HTML_TAG_PATTERN = /<[a-z/!][^>]*>/i;
|
||||
const HTML_TAG_PATTERN = /<[a-z/!?][^>]*>/i;
|
||||
|
||||
export async function settingsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// GET /api/v1/settings — Get all settings as a key-value object
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
@@ -8,6 +8,7 @@ import { readBarcodes } from "zxing-wasm/reader";
|
||||
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";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -93,7 +94,7 @@ export function registerBarcodeRead(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
} else if (part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { basename, extname } from "node:path";
|
||||
import { extname } from "node:path";
|
||||
import archiver from "archiver";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
pattern: z.string().min(1).max(1000).default("image-{{index}}"),
|
||||
@@ -31,7 +32,7 @@ export function registerBulkRename(app: FastifyInstance) {
|
||||
if (buf.length > 0) {
|
||||
files.push({
|
||||
buffer: buf,
|
||||
filename: basename(part.filename ?? `file-${files.length}`),
|
||||
filename: sanitizeFilename(part.filename ?? `file-${files.length}`),
|
||||
});
|
||||
}
|
||||
} else if (part.fieldname === "settings") {
|
||||
@@ -89,7 +90,7 @@ export function registerBulkRename(app: FastifyInstance) {
|
||||
.replace(/\{\{padded\}\}/g, padded)
|
||||
.replace(/\{\{original\}\}/g, files[i].filename.replace(ext, "")) + ext;
|
||||
|
||||
archive.append(files[i].buffer, { name: basename(newName) });
|
||||
archive.append(files[i].buffer, { name: sanitizeFilename(newName) });
|
||||
}
|
||||
|
||||
await archive.finalize();
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
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";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -435,7 +436,7 @@ export function registerCollage(app: FastifyInstance) {
|
||||
if (buf.length > 0) {
|
||||
files.push({
|
||||
buffer: buf,
|
||||
filename: basename(part.filename ?? `image-${files.length}`),
|
||||
filename: sanitizeFilename(part.filename ?? `image-${files.length}`),
|
||||
});
|
||||
}
|
||||
} else if (part.fieldname === "settings") {
|
||||
@@ -485,6 +486,10 @@ export function registerCollage(app: FastifyInstance) {
|
||||
return reply.status(400).send({ error: `Unknown template: ${settings.templateId}` });
|
||||
}
|
||||
|
||||
if (files.length > template.imageCount) {
|
||||
files.length = template.imageCount;
|
||||
}
|
||||
|
||||
// Determine output canvas size
|
||||
const BASE_SIZE = 2400;
|
||||
const arMultiplier = getAspectMultiplier(settings.aspectRatio);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { basename } from "node:path";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ export function registerColorPalette(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { seamCarve } from "@snapotter/ai";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
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 { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
@@ -41,7 +42,7 @@ export function registerContentAwareResize(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
} else if (part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
writeMetadata,
|
||||
} from "../../lib/exiftool.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { registerToolProcessFn } from "../tool-factory.js";
|
||||
@@ -83,7 +84,7 @@ export function registerEditMetadata(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -124,7 +125,7 @@ export function registerEditMetadata(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
} else if (part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
}
|
||||
|
||||
@@ -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}/`;
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { basename } from "node:path";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -108,7 +108,7 @@ export function registerFindDuplicates(app: FastifyInstance) {
|
||||
if (buf.length > 0) {
|
||||
files.push({
|
||||
buffer: buf,
|
||||
filename: basename(part.filename ?? `image-${files.length}`),
|
||||
filename: sanitizeFilename(part.filename ?? `image-${files.length}`),
|
||||
originalSize: buf.length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import PDFDocument from "pdfkit";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -109,7 +110,7 @@ export function registerImageToPdf(app: FastifyInstance) {
|
||||
if (buf.length > 0) {
|
||||
files.push({
|
||||
buffer: buf,
|
||||
filename: basename(part.filename ?? `image-${files.length}`),
|
||||
filename: sanitizeFilename(part.filename ?? `image-${files.length}`),
|
||||
});
|
||||
}
|
||||
} else if (part.fieldname === "settings") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { basename } from "node:path";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
|
||||
@@ -23,7 +23,7 @@ export function registerInfo(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { basename } from "node:path";
|
||||
import { extractText } from "@snapotter/ai";
|
||||
import { getBundleForTool, TOOL_BUNDLE_MAP } from "@snapotter/shared";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
@@ -7,6 +6,7 @@ import { z } from "zod";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { isToolInstalled } from "../../lib/feature-status.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
import { updateSingleFileProgress } from "../progress.js";
|
||||
|
||||
@@ -50,7 +50,7 @@ export function registerOcr(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
} else if (part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
} else if (part.fieldname === "clientJobId") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { detectFaceLandmarks, removeBackground } from "@snapotter/ai";
|
||||
import {
|
||||
getBundleForTool,
|
||||
@@ -15,6 +15,7 @@ import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { isToolInstalled } from "../../lib/feature-status.js";
|
||||
import { validateImageBuffer } from "../../lib/file-validation.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { decodeToSharpCompat, needsCliDecode } from "../../lib/format-decoders.js";
|
||||
import { decodeHeic } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace, getWorkspacePath } from "../../lib/workspace.js";
|
||||
@@ -154,7 +155,7 @@ export function registerPassportPhoto(app: FastifyInstance) {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of part.file) chunks.push(chunk);
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
} else if (part.fieldname === "clientJobId") {
|
||||
clientJobId = part.value as string;
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ export function registerRemoveBackground(app: FastifyInstance) {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of part.file) chunks.push(chunk);
|
||||
bgImageBuffer = Buffer.concat(chunks);
|
||||
bgFilename = part.filename ?? "background";
|
||||
bgFilename = sanitizeFilename(part.filename ?? "background");
|
||||
} else if (part.type === "field" && part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { basename, extname } from "node:path";
|
||||
import { extname } from "node:path";
|
||||
import archiver from "archiver";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { registerToolProcessFn } from "../tool-factory.js";
|
||||
|
||||
@@ -49,7 +50,7 @@ export function registerSplit(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
} else if (part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
@@ -8,6 +8,7 @@ import { env } from "../../config.js";
|
||||
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";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -58,7 +59,7 @@ export function registerStitch(app: FastifyInstance) {
|
||||
if (buf.length > 0) {
|
||||
files.push({
|
||||
buffer: buf,
|
||||
filename: basename(part.filename ?? `image-${files.length}`),
|
||||
filename: sanitizeFilename(part.filename ?? `image-${files.length}`),
|
||||
});
|
||||
}
|
||||
} else if (part.fieldname === "settings") {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { basename } from "node:path";
|
||||
import { parseExif, parseGps, parseXmp, stripMetadata } from "@snapotter/image-engine";
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { createToolRoute } from "../tool-factory.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -110,7 +110,7 @@ export function registerStripMetadata(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "image");
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import archiver from "archiver";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import PQueue from "p-queue";
|
||||
@@ -89,7 +89,7 @@ async function convertSvg(
|
||||
break;
|
||||
}
|
||||
|
||||
const baseName = basename(filename).replace(/\.svg$/i, "");
|
||||
const baseName = sanitizeFilename(filename).replace(/\.svg$/i, "");
|
||||
return { buffer, filename: `${baseName}.${ext}`, ext };
|
||||
}
|
||||
|
||||
@@ -331,7 +331,7 @@ export function registerSvgToRaster(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "output").replace(/\.svg$/i, "");
|
||||
filename = sanitizeFilename(part.filename ?? "output").replace(/\.svg$/i, "");
|
||||
} else if (part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { basename, join } from "node:path";
|
||||
import { join } from "node:path";
|
||||
import { vectorize as vtrace } from "@neplex/vectorizer";
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import potrace from "potrace";
|
||||
@@ -8,6 +8,7 @@ import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
import { createWorkspace } from "../../lib/workspace.js";
|
||||
|
||||
@@ -62,7 +63,7 @@ export function registerVectorize(app: FastifyInstance) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
fileBuffer = Buffer.concat(chunks);
|
||||
filename = basename(part.filename ?? "output").replace(/\.[^.]+$/, "");
|
||||
filename = sanitizeFilename(part.filename ?? "output").replace(/\.[^.]+$/, "");
|
||||
} else if (part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import sharp from "sharp";
|
||||
import { z } from "zod";
|
||||
import { autoOrient } from "../../lib/auto-orient.js";
|
||||
import { formatZodErrors } from "../../lib/errors.js";
|
||||
import { sanitizeFilename } from "../../lib/filename.js";
|
||||
import { ensureSharpCompat } from "../../lib/heic-converter.js";
|
||||
|
||||
const settingsSchema = z.object({
|
||||
@@ -34,7 +35,7 @@ export function registerWatermarkImage(app: FastifyInstance) {
|
||||
watermarkBuffer = buf;
|
||||
} else {
|
||||
mainBuffer = buf;
|
||||
filename = part.filename ?? "image";
|
||||
filename = sanitizeFilename(part.filename ?? "image");
|
||||
}
|
||||
} else if (part.fieldname === "settings") {
|
||||
settingsRaw = part.value as string;
|
||||
|
||||
@@ -124,7 +124,8 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
}
|
||||
|
||||
if (search) {
|
||||
conditions.push(like(schema.userFiles.originalName, `%${search}%`));
|
||||
const escaped = search.replace(/[%_\\]/g, "\\$&");
|
||||
conditions.push(like(schema.userFiles.originalName, `%${escaped}%`));
|
||||
}
|
||||
|
||||
const rows = db
|
||||
@@ -335,7 +336,9 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
|
||||
const stream = createReadStream(filePath);
|
||||
stream.on("error", () => {
|
||||
reply.status(404).send({ error: "File not found on disk" });
|
||||
if (!reply.raw.headersSent) {
|
||||
reply.status(404).send({ error: "File not found on disk" });
|
||||
}
|
||||
});
|
||||
|
||||
return reply
|
||||
@@ -356,11 +359,14 @@ export async function userFileRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get(
|
||||
"/api/v1/files/:id/thumbnail",
|
||||
async (request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) => {
|
||||
const user = requireAuth(request, reply);
|
||||
if (!user) return;
|
||||
|
||||
const { id } = request.params;
|
||||
|
||||
const file = db.select().from(schema.userFiles).where(eq(schema.userFiles.id, id)).get();
|
||||
|
||||
if (!file) {
|
||||
if (!file || (file.userId !== user.id && !hasEffectivePermission(user, "files:all"))) {
|
||||
return reply.status(404).send({ error: "File not found" });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user