From a430e913245f6c83194cc1e47c34a327d80840d5 Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Mon, 6 Apr 2026 19:40:15 +0800 Subject: [PATCH] fix: filter unsafe round-trip keys server-side in editMetadata Prevent silent data corruption when the API is called directly (bypassing UI guards). Binary/complex EXIF fields like MakerNote are now filtered from fieldsToRemove in the image-engine operation. --- .../src/operations/edit-metadata.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/image-engine/src/operations/edit-metadata.ts b/packages/image-engine/src/operations/edit-metadata.ts index c2e9f525..39b52992 100644 --- a/packages/image-engine/src/operations/edit-metadata.ts +++ b/packages/image-engine/src/operations/edit-metadata.ts @@ -2,6 +2,27 @@ import exifReader from "exif-reader"; import type { EditMetadataOptions, Sharp } from "../types.js"; import { sanitizeValue } from "../utils/metadata.js"; +/** + * Keys that are binary blobs or complex arrays - NOT safe for EXIF round-trip + * through withExif(). Silently filtered from fieldsToRemove to prevent + * data corruption when the API is called directly (bypassing UI guards). + */ +const UNSAFE_ROUND_TRIP_KEYS = new Set([ + "MakerNote", + "PrintImageMatching", + "ComponentsConfiguration", + "FlashpixVersion", + "ExifVersion", + "FileSource", + "SceneType", + "UserComment", + "InteroperabilityIndex", + "InteroperabilityVersion", + "ExifTag", + "GPSTag", + "InteroperabilityTag", +]); + const COMMON_FIELD_MAP: Array<{ option: keyof EditMetadataOptions; ifd: "IFD0" | "IFD2"; @@ -32,7 +53,9 @@ export async function editMetadata( } const writtenTags = new Set([...Object.keys(edits.IFD0), ...Object.keys(edits.IFD2)]); - const fieldsToRemove = (options.fieldsToRemove ?? []).filter((f) => !writtenTags.has(f)); + const fieldsToRemove = (options.fieldsToRemove ?? []).filter( + (f) => !writtenTags.has(f) && !UNSAFE_ROUND_TRIP_KEYS.has(f), + ); const hasEdits = Object.keys(edits.IFD0).length > 0 || Object.keys(edits.IFD2).length > 0; const hasRemovals = fieldsToRemove.length > 0 || options.clearGps === true;