{"record":{"id":"58966d393da13e40","repo":"gitroomhq/postiz-app","slug":"unsafe-url","errorCode":null,"errorMessage":"Unsafe URL","messagePattern":"Unsafe URL","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"libraries/nestjs-libraries/src/upload/cloudflare.storage.ts","lineNumber":88,"sourceCode":"              (request.headers as Record<string, string>)[key] = value;\n            }\n          );\n\n          return next(args);\n        },\n      { step: 'build', name: 'customHeaders' }\n    );\n  }\n\n  async uploadSimple(path: string) {\n    const dataUrl = path.startsWith('data:') ? parseDataUrl(path) : null;\n\n    let body: Buffer;\n    if (dataUrl) {\n      body = dataUrl.buffer;\n    } 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    const detected = await fileTypeFromBuffer(body);\n    if (!detected || !ALLOWED_MIME_TYPES.has(detected.mime)) {\n      throw new Error('Unsupported file type.');\n    }\n    const extension = detected.ext;\n    const safeContentType = detected.mime;\n    const id = makeId(10);\n\n    const params = {\n      Bucket: this._bucketName,\n      Key: `${id}.${extension}`,","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/gitroomhq/postiz-app/blob/0f1647f7491a217d43eb5ae7a480484bdf0aff3e/libraries/nestjs-libraries/src/upload/cloudflare.storage.ts#L70-L106","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait storage.uploadSimple('http://cdn.example.com/pic.png'); // throws Unsafe URL\n\n// after\nawait storage.uploadSimple('https://cdn.example.com/pic.png');","handlingStrategy":"validation","validationCode":"import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator';\n\nconst safe = path.startsWith('data:') || (await isSafePublicHttpsUrl(path));\nif (!safe) {\n  throw new BadRequestException('Only public https URLs or data URLs can be uploaded');\n}","typeGuard":"function isUploadableUrl(u: string): boolean {\n  if (u.startsWith('data:')) return true;\n  try { return new URL(u).protocol === 'https:'; } catch { return false; }\n}","tryCatchPattern":"try {\n  url = await storage.uploadSimple(path);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Unsafe URL') {\n    throw new BadRequestException('URL must be a public https URL');\n  }\n  throw e;\n}","preventionTips":["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"],"tags":["ssrf","security","upload","url-validation","cloudflare-r2"],"backgroundTag":"ssrf-url-blocked","analyzedSha":"0f1647f7491a217d43eb5ae7a480484bdf0aff3e","analyzedAt":"2026-08-27T12:09:55.020Z","schemaVersion":2},"datasetVersion":"2026-08-27T13:17:12.746Z"}