gitroomhq/postiz-app · error · Error
Unsafe URL
Error message
Unsafe URL
What it means
Thrown by CloudflareStorage.uploadSimple when the input path is a remote URL (not a data: URL) that fails the isSafePublicHttpsUrl SSRF check. This is a deliberate security guard: only public HTTPS URLs are fetched, blocking private/reserved IP ranges, non-HTTPS schemes, and hosts that resolve to internal addresses.
Source
Thrown at libraries/nestjs-libraries/src/upload/cloudflare.storage.ts:88
(request.headers as Record<string, string>)[key] = value;
}
);
return next(args);
},
{ step: 'build', name: 'customHeaders' }
);
}
async uploadSimple(path: string) {
const dataUrl = path.startsWith('data:') ? parseDataUrl(path) : null;
let body: Buffer;
if (dataUrl) {
body = dataUrl.buffer;
} 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());
}
const detected = await fileTypeFromBuffer(body);
if (!detected || !ALLOWED_MIME_TYPES.has(detected.mime)) {
throw new Error('Unsupported file type.');
}
const extension = detected.ext;
const safeContentType = detected.mime;
const id = makeId(10);
const params = {
Bucket: this._bucketName,
Key: `${id}.${extension}`,View on GitHub (pinned to 0f1647f749)
Solutions
- Use an https:// URL — http:// is always rejected
- Ensure the host resolves to a public IP (no split-horizon DNS pointing to internal ranges)
- If developing locally with service names/localhost, upload a data: URL or a real public URL instead
- Do not bypass the check; it exists to prevent SSRF against internal services
Example fix
// before
await storage.uploadSimple('http://cdn.example.com/pic.png'); // throws Unsafe URL
// after
await storage.uploadSimple('https://cdn.example.com/pic.png'); Defensive patterns
Strategy: validation
Validate before calling
import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator';
const safe = path.startsWith('data:') || (await isSafePublicHttpsUrl(path));
if (!safe) {
throw new BadRequestException('Only public https URLs or data URLs can be uploaded');
} Type guard
function isUploadableUrl(u: string): boolean {
if (u.startsWith('data:')) return true;
try { return new URL(u).protocol === 'https:'; } catch { return false; }
} Try / catch
try {
url = await storage.uploadSimple(path);
} catch (e) {
if (e instanceof Error && e.message === 'Unsafe URL') {
throw new BadRequestException('URL must be a public https URL');
}
throw e;
} Prevention
- Normalize user-supplied media URLs to https before persisting them
- Validate scheme and host at the DTO/controller layer with a class-validator @IsUrl rule
- Never whitelist 'localhost' or private ranges to 'fix' this — upgrade to https or use data URLs
When it happens
Trigger: Calling uploadSimple('http://example.com/img.png') (plain http), a URL whose DNS resolves to a private IP (10.x/127.x/169.254.x/etc.), a URL with credentials or unusual scheme, or an invalid URL string.
Common situations: User-supplied media URLs from social providers that still use http://; localhost/internal URLs passed during local dev; URLs behind DNS that intermittently resolves to internal ranges; test fixtures using http://test-host.
Related errors
AI-assisted analysis of gitroomhq/postiz-app@0f1647f749 (2026-08-27).
Data as JSON: /api/errors/58966d393da13e40.
Report an issue: GitHub.