{"record":{"id":"ec60e8131c8839f0","repo":"gitroomhq/postiz-app","slug":"unsupported-file-type-ec60e8","errorCode":null,"errorMessage":"Unsupported file type.","messagePattern":"Unsupported file type\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"libraries/nestjs-libraries/src/upload/local.storage.ts","lineNumber":50,"sourceCode":"    } else {\n      if (!(await isSafePublicHttpsUrl(path))) {\n        throw new Error('Unsafe URL');\n      }\n      const loadImage = await fetch(path, {\n        // @ts-ignore — undici option, not in lib.dom fetch types\n        dispatcher: ssrfSafeDispatcher,\n      });\n      body = Buffer.from(await loadImage.arrayBuffer());\n    }\n\n    // Never trust the claimed mime/extension (data URL header, remote\n    // content-type, or URL path): sniff the real type from the bytes and\n    // only accept the allow-list, otherwise an attacker could write an\n    // arbitrary file (e.g. .html/.svg with embedded script) into the\n    // publicly served uploads directory on the app's own origin.\n    const detected = await fileTypeFromBuffer(body);\n    if (!detected || !LOCAL_STORAGE_ALLOWED_MIME.has(detected.mime)) {\n      throw new Error('Unsupported file type.');\n    }\n    const findExtension = detected.ext;\n\n    const now = new Date();\n    const year = now.getFullYear();\n    const month = String(now.getMonth() + 1).padStart(2, '0');\n    const day = String(now.getDate()).padStart(2, '0');\n\n    const innerPath = `/${year}/${month}/${day}`;\n    const dir = `${this.uploadDirectory}${innerPath}`;\n    mkdirSync(dir, { recursive: true });\n\n    const randomName = Array(32)\n      .fill(null)\n      .map(() => Math.round(Math.random() * 16).toString(16))\n      .join('');\n\n    const filePath = `${dir}/${randomName}.${findExtension}`;","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/upload/local.storage.ts#L32-L68","documentation":"After fetching a remote URL, uploadSimple re-sniffs the bytes and requires the detected MIME to be in LOCAL_STORAGE_ALLOWED_MIME. Because local storage serves files from the app's own origin, this blocks writing dangerous content (HTML/SVG with script) that could enable stored XSS.","triggerScenarios":"A URL returning text/html, image/svg+xml, or any type outside the local allow-list; a 'broken image' URL that actually returns an HTML error page (404/403 page); content negotiation returning something unexpected; a file with no recognizable magic bytes.","commonSituations":"Passing a webpage URL instead of a direct image URL; hotlink-protected CDNs returning an error page; SVG assets; servers returning generic application/octet-stream.","solutions":["Use a direct link to the actual image/video bytes, not a page or redirect-to-HTML","Check `curl -sI <url>` and confirm the body is a real image (run `file` on `curl -s <url>` output)","Convert the asset locally and upload the bytes via the dataUrl/buffer path instead of URL fetching","If a type is genuinely needed, add it to LOCAL_STORAGE_ALLOWED_MIME only with a serving-strategy review (e.g. force download or separate origin)"],"exampleFix":"// before\nawait localStorage.uploadSimple(undefined, 'https://example.com/profile'); // HTML page\n\n// after\nawait localStorage.uploadSimple(undefined, 'https://cdn.example.com/img/profile.png');","handlingStrategy":"validation","validationCode":"const resp = await fetch(url);\nconst buf = Buffer.from(await resp.arrayBuffer());\nconst t = await fileTypeFromBuffer(buf);\nif (!t || !LOCAL_ALLOWED.has(t.mime)) throw new Error('URL does not point to an allowed media file');","typeGuard":"const pointsToAllowedMedia = async (u: string) => { const r = await fetch(u); const t = await fileTypeFromBuffer(Buffer.from(await r.arrayBuffer())); return !!t && LOCAL_ALLOWED.has(t.mime); };","tryCatchPattern":"try { await uploadSimple(undefined, url); } catch (e) { if (/Unsupported file type/.test(String(e))) downloadLocallyAndUploadBuffer(url); else throw e; }","preventionTips":["Link directly to media bytes, not HTML pages","Check the URL with curl before wiring it in","Prefer uploading bytes over fetching arbitrary URLs"],"tags":["upload","file-type","xss-prevention","local-storage"],"backgroundTag":"unsupported-file-type","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}