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

  1. Use a direct link to the actual image/video bytes, not a page or redirect-to-HTML
  2. Check `curl -sI <url>` and confirm the body is a real image (run `file` on `curl -s <url>` output)
  3. Convert the asset locally and upload the bytes via the dataUrl/buffer path instead of URL fetching
  4. 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

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


AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27). Data as JSON: /api/errors/ec60e8131c8839f0. Report an issue: GitHub.