gitroomhq/postiz-app · error · Error
Unsupported file type.
Error message
Unsupported file type.
What it means
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.
Source
Thrown at libraries/nestjs-libraries/src/upload/local.storage.ts:50
} else {
if (!(await isSafePublicHttpsUrl(path))) {
throw new Error('Unsafe URL');
}
const loadImage = await fetch(path, {
// @ts-ignore — undici option, not in lib.dom fetch types
dispatcher: ssrfSafeDispatcher,
});
body = Buffer.from(await loadImage.arrayBuffer());
}
// Never trust the claimed mime/extension (data URL header, remote
// content-type, or URL path): sniff the real type from the bytes and
// only accept the allow-list, otherwise an attacker could write an
// arbitrary file (e.g. .html/.svg with embedded script) into the
// publicly served uploads directory on the app's own origin.
const detected = await fileTypeFromBuffer(body);
if (!detected || !LOCAL_STORAGE_ALLOWED_MIME.has(detected.mime)) {
throw new Error('Unsupported file type.');
}
const findExtension = detected.ext;
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const innerPath = `/${year}/${month}/${day}`;
const dir = `${this.uploadDirectory}${innerPath}`;
mkdirSync(dir, { recursive: true });
const randomName = Array(32)
.fill(null)
.map(() => Math.round(Math.random() * 16).toString(16))
.join('');
const filePath = `${dir}/${randomName}.${findExtension}`;View on GitHub (pinned to 0f1647f749)
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)
Example fix
// before await localStorage.uploadSimple(undefined, 'https://example.com/profile'); // HTML page // after await localStorage.uploadSimple(undefined, 'https://cdn.example.com/img/profile.png');
Defensive patterns
Strategy: validation
Validate before calling
const resp = await fetch(url);
const buf = Buffer.from(await resp.arrayBuffer());
const t = await fileTypeFromBuffer(buf);
if (!t || !LOCAL_ALLOWED.has(t.mime)) throw new Error('URL does not point to an allowed media file'); Type guard
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); }; Try / catch
try { await uploadSimple(undefined, url); } catch (e) { if (/Unsupported file type/.test(String(e))) downloadLocallyAndUploadBuffer(url); else throw e; } Prevention
- Link directly to media bytes, not HTML pages
- Check the URL with curl before wiring it in
- Prefer uploading bytes over fetching arbitrary URLs
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Unsupported file type.
- Unsupported file type.
- Unsupported file type.
- Unsupported file type.
- Failed to fetch URL
AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27).
Data as JSON: /api/errors/ec60e8131c8839f0.
Report an issue: GitHub.