mirror of
https://github.com/penpot/penpot.git
synced 2026-10-30 06:20:32 +00:00
Address the review findings on the DOMPurify SVG sanitization. F1: sanitizeSvg was synchronous and blocked the service's only event loop: a ~30 MiB SVG (the old cap) froze media-processor for ~20 s and peaked at ~3.4 GiB. The sanitizer now runs in a worker_threads pool (sanitizeSvgSync is the CPU-bound core), each worker has a resourceLimits heap cap so an OOM kills the worker and not the process, and a stuck job is terminated by a timeout and respawned. The default SVG cap drops to 2 MiB and is enforced in both modes (the backend validates the declared and the real size on upload, and the real bytes on import), so a file is accepted or rejected the same way with and without media-processor. F2: an unexpected sanitizer failure is logged and reported as 503 svg-sanitization-failed instead of 400 invalid-svg-file, so a service fault is not blamed on the user's file and no internal detail leaks in the hint. F4: the remote backend rejects a 2xx response that is not an SVG instead of storing it. Also pin the backend/service wire contract with a test that posts the exact multipart the JVM builds, and pin the data: URI behavior. AI-assisted-by: deepseek-v4.1-flash
73 lines
4.0 KiB
TypeScript
73 lines
4.0 KiB
TypeScript
import { z } from "zod";
|
|
import { hkdfSync } from "node:crypto";
|
|
import type { AppConfig } from "./types.js";
|
|
|
|
const envSchema = z.object({
|
|
PENPOT_MEDIA_PROCESSOR_PORT: z.coerce.number().int().positive().default(6065),
|
|
PENPOT_MEDIA_PROCESSOR_HOST: z.string().default("0.0.0.0"),
|
|
PENPOT_MEDIA_PROCESSOR_MAX_CONCURRENT_REQUESTS: z.coerce.number().int().min(1).default(10),
|
|
PENPOT_MEDIA_PROCESSOR_REQUEST_TIMEOUT: z.coerce.number().int().nonnegative().default(180000),
|
|
PENPOT_MEDIA_PROCESSOR_MAX_FILE_SIZE: z.coerce.number().int().positive().default(367001600), // 350 MB
|
|
PENPOT_MEDIA_PROCESSOR_MEMORY_THRESHOLD: z.coerce.number().int().positive().default(10485760), // 10 MB — uploads below this use memory storage; above use disk storage
|
|
PENPOT_MEDIA_PROCESSOR_IMAGE_MAX_PIXELS: z.coerce.number().int().positive().default(128_000_000),
|
|
PENPOT_MEDIA_PROCESSOR_IMAGE_MAX_WIDTH: z.coerce.number().int().positive().default(16384),
|
|
PENPOT_MEDIA_PROCESSOR_IMAGE_MAX_HEIGHT: z.coerce.number().int().positive().default(16384),
|
|
PENPOT_MEDIA_PROCESSOR_SVG_MAX_SIZE: z.coerce.number().int().positive().default(2097152), // 2 MB — see mem:media-processor/core
|
|
PENPOT_MEDIA_PROCESSOR_SVG_WORKERS: z.coerce.number().int().nonnegative().default(2), // 0 = run inline (tests/escape hatch)
|
|
PENPOT_MEDIA_PROCESSOR_SVG_WORKER_MAX_OLD_MB: z.coerce.number().int().positive().default(512),
|
|
PENPOT_MEDIA_PROCESSOR_SVG_TIMEOUT: z.coerce.number().int().positive().default(30000),
|
|
PENPOT_MEDIA_PROCESSOR_FONT_PROCESS_MEM: z.coerce.number().int().positive().default(512),
|
|
PENPOT_MEDIA_PROCESSOR_FONT_PROCESS_CPU_TIME: z.coerce.number().int().positive().default(30),
|
|
PENPOT_MEDIA_PROCESSOR_FONT_TIMEOUT: z.coerce.number().int().positive().default(120000),
|
|
PENPOT_MEDIA_PROCESSOR_SHARED_KEY: z.string().optional(),
|
|
PENPOT_SECRET_KEY: z.string().optional(),
|
|
PENPOT_MEDIA_PROCESSOR_LOG_LEVEL: z
|
|
.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"])
|
|
.default("info"),
|
|
PENPOT_LOGGERS_LOKI_URI: z.string().optional(),
|
|
PENPOT_LOGGERS_LOKI_JOB: z.string().default("media-processor"),
|
|
PENPOT_LOGGERS_LOKI_ENVIRONMENT: z.string().optional(),
|
|
PENPOT_LOGGERS_LOKI_INSTANCE: z.string().optional(),
|
|
});
|
|
|
|
function deriveSharedKey(secret: string): string {
|
|
const key = hkdfSync("blake2b512", secret, Buffer.from("media-processor"), "", 32);
|
|
return Buffer.from(key).toString("base64url");
|
|
}
|
|
|
|
export function loadConfig(): AppConfig {
|
|
const parsed = envSchema.parse(process.env);
|
|
|
|
let sharedKey: string | null = null;
|
|
if (parsed.PENPOT_MEDIA_PROCESSOR_SHARED_KEY) {
|
|
sharedKey = parsed.PENPOT_MEDIA_PROCESSOR_SHARED_KEY;
|
|
} else if (parsed.PENPOT_SECRET_KEY) {
|
|
sharedKey = deriveSharedKey(parsed.PENPOT_SECRET_KEY);
|
|
}
|
|
|
|
return {
|
|
port: parsed.PENPOT_MEDIA_PROCESSOR_PORT,
|
|
host: parsed.PENPOT_MEDIA_PROCESSOR_HOST,
|
|
maxConcurrentRequests: parsed.PENPOT_MEDIA_PROCESSOR_MAX_CONCURRENT_REQUESTS,
|
|
requestTimeout: parsed.PENPOT_MEDIA_PROCESSOR_REQUEST_TIMEOUT,
|
|
maxFileSize: parsed.PENPOT_MEDIA_PROCESSOR_MAX_FILE_SIZE,
|
|
memoryThreshold: parsed.PENPOT_MEDIA_PROCESSOR_MEMORY_THRESHOLD,
|
|
imageMaxPixels: parsed.PENPOT_MEDIA_PROCESSOR_IMAGE_MAX_PIXELS,
|
|
imageMaxWidth: parsed.PENPOT_MEDIA_PROCESSOR_IMAGE_MAX_WIDTH,
|
|
imageMaxHeight: parsed.PENPOT_MEDIA_PROCESSOR_IMAGE_MAX_HEIGHT,
|
|
svgMaxSize: parsed.PENPOT_MEDIA_PROCESSOR_SVG_MAX_SIZE,
|
|
svgWorkers: parsed.PENPOT_MEDIA_PROCESSOR_SVG_WORKERS,
|
|
svgWorkerMaxOldMb: parsed.PENPOT_MEDIA_PROCESSOR_SVG_WORKER_MAX_OLD_MB,
|
|
svgTimeout: parsed.PENPOT_MEDIA_PROCESSOR_SVG_TIMEOUT,
|
|
fontProcessMem: parsed.PENPOT_MEDIA_PROCESSOR_FONT_PROCESS_MEM,
|
|
fontProcessCpuTime: parsed.PENPOT_MEDIA_PROCESSOR_FONT_PROCESS_CPU_TIME,
|
|
fontTimeout: parsed.PENPOT_MEDIA_PROCESSOR_FONT_TIMEOUT,
|
|
sharedKey,
|
|
logLevel: parsed.PENPOT_MEDIA_PROCESSOR_LOG_LEVEL,
|
|
lokiUri: parsed.PENPOT_LOGGERS_LOKI_URI || null,
|
|
lokiJob: parsed.PENPOT_LOGGERS_LOKI_JOB,
|
|
lokiEnvironment: parsed.PENPOT_LOGGERS_LOKI_ENVIRONMENT || null,
|
|
lokiInstance: parsed.PENPOT_LOGGERS_LOKI_INSTANCE || null,
|
|
};
|
|
}
|