hcengineering/platform · error · LinkPreviewError

BLOCKED_URL

BLOCKED_URL

Error message

Blocked URL: Access to internal addresses is not allowed.

What it means

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.

Source

Thrown at pods/link-preview/src/parse.ts:224

function validateUrl (urlString: string): URL {
  let url: URL
  try {
    url = new URL(urlString)
  } catch {
    throw new LinkPreviewError(`Invalid URL: ${urlString}`, 'INVALID_URL')
  }

  // Only allow HTTP(S) protocols
  if (!['http:', 'https:'].includes(url.protocol)) {
    throw new LinkPreviewError(
      `Invalid protocol: ${url.protocol}. Only HTTP and HTTPS are allowed.`,
      'INVALID_PROTOCOL'
    )
  }

  // SSRF protection: block private/internal hosts and IP literals (incl. IPv6-mapped IPv4)
  if (isBlockedHost(url.hostname)) {
    throw new LinkPreviewError('Blocked URL: Access to internal addresses is not allowed.', 'BLOCKED_URL')
  }

  return url
}

function isRedirectStatus (status: number): boolean {
  return status >= 300 && status < 400
}

async function fetchWithValidatedRedirects (
  url: string,
  options: RequestInit,
  timeoutMs: number,
  maxRedirects: number = 5
): Promise<{ response: Response, finalUrl: string }> {
  let currentUrl = url

  for (let i = 0; i <= maxRedirects; i++) {

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Use a publicly reachable URL for the resource
  2. Expose an internal mirror/proxy of the internal content and preview that instead
  3. If legitimately needed (self-hosted deployments), adjust isBlockedHost's allowlist consciously, keeping SSRF risks in mind

Example fix

// before
preview('http://localhost:8080/dashboard') // throws BLOCKED_URL
// after
preview('https://public.example.com/dashboard')
Defensive patterns

Strategy: try-catch

Validate before calling

function isPubliclyRoutable(s: string): boolean {
  try {
    const u = new URL(s.trim())
    const host = u.hostname
    return !['localhost', '127.0.0.1', '::1', '0.0.0.0'].includes(host) &&
      !/^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.)/.test(host) &&
      !/(^|\.)internal$/i.test(host)
  } catch { return false }
}

Try / catch

try {
  return await loadImageSize(client, url)
} catch (err) {
  if (err instanceof LinkPreviewError && err.code === 'BLOCKED_URL') {
    console.warn('internal address blocked (SSRF protection):', url); return null
  }
  throw err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/e15df8620469138c. Report an issue: GitHub.