{"record":{"id":"e15df8620469138c","repo":"hcengineering/platform","slug":"blocked-url","errorCode":"BLOCKED_URL","errorMessage":"Blocked URL: Access to internal addresses is not allowed.","messagePattern":"Blocked URL: Access to internal addresses is not allowed\\.","errorType":"error_code","errorClass":"LinkPreviewError","httpStatus":null,"severity":"error","filePath":"pods/link-preview/src/parse.ts","lineNumber":224,"sourceCode":"function validateUrl (urlString: string): URL {\n  let url: URL\n  try {\n    url = new URL(urlString)\n  } catch {\n    throw new LinkPreviewError(`Invalid URL: ${urlString}`, 'INVALID_URL')\n  }\n\n  // Only allow HTTP(S) protocols\n  if (!['http:', 'https:'].includes(url.protocol)) {\n    throw new LinkPreviewError(\n      `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`,\n      'INVALID_PROTOCOL'\n    )\n  }\n\n  // SSRF protection: block private/internal hosts and IP literals (incl. IPv6-mapped IPv4)\n  if (isBlockedHost(url.hostname)) {\n    throw new LinkPreviewError('Blocked URL: Access to internal addresses is not allowed.', 'BLOCKED_URL')\n  }\n\n  return url\n}\n\nfunction isRedirectStatus (status: number): boolean {\n  return status >= 300 && status < 400\n}\n\nasync function fetchWithValidatedRedirects (\n  url: string,\n  options: RequestInit,\n  timeoutMs: number,\n  maxRedirects: number = 5\n): Promise<{ response: Response, finalUrl: string }> {\n  let currentUrl = url\n\n  for (let i = 0; i <= maxRedirects; i++) {","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/pods/link-preview/src/parse.ts#L206-L242","documentation":"As SSRF protection, validateUrl calls isBlockedHost on the hostname, rejecting private/internal addresses and IP literals (incl. IPv6-mapped IPv4) with LinkPreviewError code BLOCKED_URL.","triggerScenarios":"Requesting previews for 'http://localhost:3000', 'http://127.0.0.1', 'http://192.168.1.10', 'http://[::1]', or '.internal'/metadata hostnames via fetchOEmbedData / loadImageSize / parsedUrl.","commonSituations":"Internal dashboards or intranet links pasted by users; redirect-based SSRF probes to 169.254.169.254 cloud metadata; local dev URLs shared into a preview field; corporate hostnames resolving to private IPs.","solutions":["Use a publicly reachable URL for the resource","Expose an internal mirror/proxy of the internal content and preview that instead","If legitimately needed (self-hosted deployments), adjust isBlockedHost's allowlist consciously, keeping SSRF risks in mind"],"exampleFix":"// before\npreview('http://localhost:8080/dashboard') // throws BLOCKED_URL\n// after\npreview('https://public.example.com/dashboard')","handlingStrategy":"try-catch","validationCode":"function isPubliclyRoutable(s: string): boolean {\n  try {\n    const u = new URL(s.trim())\n    const host = u.hostname\n    return !['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(host) &&\n      !/^(10\\.|172\\.(1[6-9]|2\\d|3[01])\\.|192\\.168\\.|169\\.254\\.)/.test(host) &&\n      !/(^|\\.)internal$/i.test(host)\n  } catch { return false }\n}","typeGuard":null,"tryCatchPattern":"try {\n  return await loadImageSize(client, url)\n} catch (err) {\n  if (err instanceof LinkPreviewError && err.code === 'BLOCKED_URL') {\n    console.warn('internal address blocked (SSRF protection):', url); return null\n  }\n  throw err\n}","preventionTips":["Never preview localhost/private-range/intranet URLs","Filter private IPs and .internal hostnames in the UI before submit","Be aware redirects can also target internal hosts — the library validates redirect targets too","For intranet content, publish a public mirror instead"],"tags":["ssrf","security","url","validation"],"backgroundTag":"ssrf-blocked-host","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}