From b64dcf9b7a34706f9ff08abc37471f9250b257d3 Mon Sep 17 00:00:00 2001 From: oasis-bot Date: Mon, 19 Oct 2026 02:02:16 +0200 Subject: [PATCH] fotos: si no se pueden limpiar, no se suben Si quitar los metadatos fallaba, blobHandler subia la foto original, con el GPS. Ahora se para con un 400 y un aviso (imageMetadataUnsafe, en los 11 idiomas): HEIC/HEIF/AVIF y TIFF, que pueden llevar EXIF y aqui no se saben limpiar, y JPEG/PNG/WebP que no se dejan recorrer. Un JPEG cortado ya no sube como una imagen vacia. Formatos sin EXIF (BMP, SVG...) pasan igual. Es el cambio del 10 de octubre, llevado a la copia de la limpieza que vive ahora en blobHandler.js. --- src/backend/blobHandler.js | 45 +++++++++++++++++---- src/client/assets/translations/i18n_fork.js | 22 ++++++++++ 2 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/backend/blobHandler.js b/src/backend/blobHandler.js index 0de44ea..cb2c3c2 100644 --- a/src/backend/blobHandler.js +++ b/src/backend/blobHandler.js @@ -85,11 +85,12 @@ const stripJpeg = (buf) => { const out = [buf.subarray(0, 2)]; let orientation = 0; let i = 2; + let scan = false; while (i + 4 <= buf.length) { if (buf[i] !== 0xFF) return null; // no es un JPEG sano: no tocarlo const marker = buf[i + 1]; if (marker === 0xD8 || (marker >= 0xD0 && marker <= 0xD9)) { i += 2; continue; } - if (marker === 0xDA) { out.push(buf.subarray(i)); break; } // datos comprimidos: hasta el final + if (marker === 0xDA) { out.push(buf.subarray(i)); scan = true; break; } // datos comprimidos: hasta el final const len = buf.readUInt16BE(i + 2); if (len < 2 || i + 2 + len > buf.length) return null; const seg = buf.subarray(i + 4, i + 2 + len); @@ -97,6 +98,8 @@ const stripJpeg = (buf) => { if (!JPEG_DROP.has(marker)) out.push(buf.subarray(i, i + 2 + len)); i += 2 + len; } + // sin datos de imagen (SOS) es un JPEG cortado: devolver la cabecera sola subiria una foto vacia + if (!scan) return null; if (orientation > 1) { const exif = exifOnlyOrientation(orientation); const head = Buffer.alloc(4); @@ -150,14 +153,40 @@ const stripWebp = (buf) => { return Buffer.concat([head, body]); }; -/** Limpia segun el formato; si algo no cuadra, devuelve la imagen tal cual. */ -const stripMetadataPure = (buffer) => { - try { - const out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer); - return out && out.length ? out : buffer; - } catch { - return buffer; +// Formatos que pueden llevar EXIF (GPS incluido) y que aqui no se saben limpiar: +// HEIC/HEIF/AVIF (caja ftyp) y TIFF. Antes subian tal cual, con la ubicacion. +const isUncleanable = (buf) => { + if (buf.length >= 12 && buf.toString("latin1", 4, 8) === "ftyp") { + return /^(heic|heix|hevc|hevx|heim|heis|mif1|msf1|avif|avis)$/.test(buf.toString("latin1", 8, 12)); } + return buf.length >= 4 && (buf.toString("latin1", 0, 4) === "II*\0" || buf.toString("latin1", 0, 4) === "MM\0*"); +}; +const looksCleanable = (buf) => + (buf.length >= 2 && buf[0] === 0xFF && buf[1] === 0xD8) || + (buf.length >= 8 && buf.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]))) || + (buf.length >= 12 && buf.toString("latin1", 0, 4) === "RIFF" && buf.toString("latin1", 8, 12) === "WEBP"); + +const unsafe = () => { + let msg = "This image could not be cleaned of its metadata, so it has not been uploaded."; + try { msg = require("../views/main_views").i18n.imageMetadataUnsafe || msg; } catch (_) {} + const err = new Error(msg); + err.status = 400; + return err; +}; + +/** + * Limpia segun el formato. Si es JPEG/PNG/WebP y no se puede limpiar, o es un + * formato con EXIF que no sabemos limpiar, LANZA: es mejor no subir la foto que + * subirla con la ubicacion. Formatos sin metadatos de ese tipo (BMP, SVG, ICO...) + * pasan tal cual. + */ +const stripMetadataPure = (buffer) => { + if (isUncleanable(buffer)) throw unsafe(); + if (!looksCleanable(buffer)) return buffer; + let out = null; + try { out = stripJpeg(buffer) || stripPng(buffer) || stripWebp(buffer); } catch (_) { out = null; } + if (!out || !out.length) throw unsafe(); + return out; }; const stripImageMetadata = async (buffer) => { diff --git a/src/client/assets/translations/i18n_fork.js b/src/client/assets/translations/i18n_fork.js index 35b2dc4..6eb8379 100644 --- a/src/client/assets/translations/i18n_fork.js +++ b/src/client/assets/translations/i18n_fork.js @@ -17,6 +17,8 @@ module.exports = { en: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "This image could not be cleaned of its metadata (location, camera…), so it has not been uploaded. Save it as JPEG or PNG and try again.", // Karvan karvanActive: "Active rooms", karvanBlurb: "Rooms that live only in memory and destroy themselves.", @@ -80,6 +82,8 @@ module.exports = { spreadMore: "more" }, es: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "No se han podido quitar los metadatos de esta imagen (ubicación, cámara…), así que no se ha subido. Guárdala como JPEG o PNG y vuelve a intentarlo.", // Karvan karvanActive: "Salas activas", karvanBlurb: "Salas que viven solo en memoria y se destruyen solas.", @@ -149,6 +153,8 @@ module.exports = { totalspreads: "Réplicas totales" }, fr: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Impossible de retirer les métadonnées de cette image (position, appareil…), elle n'a donc pas été envoyée. Enregistrez-la en JPEG ou PNG et réessayez.", // Karvan karvanActive: "Salons actifs", karvanBlurb: "Des salons qui ne vivent qu'en mémoire et se détruisent d'eux-mêmes.", @@ -212,6 +218,8 @@ module.exports = { spreadMore: "plus" }, eu: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Ezin izan dira irudi honen metadatuak kendu (kokapena, kamera…), beraz ez da igo. Gorde JPEG edo PNG gisa eta saiatu berriro.", // Karvan karvanActive: "Gela aktiboak", karvanBlurb: "Memorian bakarrik bizi diren eta beren kabuz suntsitzen diren gelak.", @@ -275,6 +283,8 @@ module.exports = { spreadMore: "gehiago" }, de: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Die Metadaten dieses Bildes (Standort, Kamera…) konnten nicht entfernt werden, daher wurde es nicht hochgeladen. Speichere es als JPEG oder PNG und versuche es erneut.", // Karvan karvanActive: "Aktive Räume", karvanBlurb: "Räume, die nur im Speicher leben und sich von selbst zerstören.", @@ -338,6 +348,8 @@ module.exports = { spreadMore: "mehr" }, it: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Non è stato possibile rimuovere i metadati di questa immagine (posizione, fotocamera…), quindi non è stata caricata. Salvala come JPEG o PNG e riprova.", // Karvan karvanActive: "Stanze attive", karvanBlurb: "Stanze che vivono solo in memoria e si distruggono da sole.", @@ -401,6 +413,8 @@ module.exports = { spreadMore: "altro" }, pt: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Não foi possível remover os metadados desta imagem (localização, câmara…), por isso não foi enviada. Guarda-a como JPEG ou PNG e tenta de novo.", // Karvan karvanActive: "Salas ativas", karvanBlurb: "Salas que vivem apenas em memória e destroem-se sozinhas.", @@ -464,6 +478,8 @@ module.exports = { spreadMore: "mais" }, zh: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "无法清除这张图片的元数据(位置、相机等),因此没有上传。请另存为 JPEG 或 PNG 后重试。", // Karvan karvanActive: "活跃房间", karvanBlurb: "只存在于内存中、会自行销毁的房间。", @@ -527,6 +543,8 @@ module.exports = { spreadMore: "更多" }, ar: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "تعذّر إزالة البيانات الوصفية من هذه الصورة (الموقع، الكاميرا…)، لذلك لم تُرفع. احفظها بصيغة JPEG أو PNG وحاول مجددًا.", // Karvan karvanActive: "الغرف النشطة", karvanBlurb: "غرف تعيش في الذاكرة فقط وتدمّر نفسها بنفسها.", @@ -590,6 +608,8 @@ module.exports = { spreadMore: "المزيد" }, hi: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "इस छवि का मेटाडेटा (स्थान, कैमरा…) हटाया नहीं जा सका, इसलिए इसे अपलोड नहीं किया गया। इसे JPEG या PNG के रूप में सहेजें और फिर कोशिश करें।", // Karvan karvanActive: "सक्रिय कक्ष", karvanBlurb: "ऐसे कक्ष जो केवल मेमोरी में रहते हैं और स्वयं नष्ट हो जाते हैं।", @@ -653,6 +673,8 @@ module.exports = { spreadMore: "और" }, ru: { + // fotos que no se pueden limpiar (blobHandler.js) + imageMetadataUnsafe: "Не удалось удалить метаданные этого изображения (местоположение, камера…), поэтому оно не загружено. Сохраните его как JPEG или PNG и попробуйте снова.", // Karvan karvanActive: "Активные комнаты", karvanBlurb: "Комнаты, которые живут только в памяти и уничтожаются сами.",