Crosstalk-Solutions/project-nomad · warning

${error.message}

Error message

${error.message}

What it means

browseLibraryUrl performs SSRF protection and rejects any URL resolving to loopback (127.0.0.1, ::1) or link-local (169.254.x.x, fe80::/10) addresses. The original service error message is surfaced verbatim with a 400 so the caller knows exactly which address class was blocked.

Source

Thrown at admin/app/controllers/zim_controller.ts:222

    const payload = await request.validateUsing(idParamValidator)
    try {
      await this.zimService.removeCustomLibrary(payload.params.id)
      return { message: 'Custom library removed' }
    } catch (error) {
      if (error.message === 'Custom library not found') {
        return response.status(404).send({ message: error.message })
      }
      throw error
    }
  }

  async browseLibrary({ request, response }: HttpContext) {
    const payload = await request.validateUsing(browseLibraryValidator)
    try {
      return await this.zimService.browseLibraryUrl(payload.url)
    } catch (error) {
      if (error.message?.includes('loopback or link-local')) {
        return response.status(400).send({ message: error.message })
      }
      return response.status(502).send({
        message: 'Could not fetch directory listing from the provided URL',
      })
    }
  }
}

View on GitHub (pinned to 0bd1c6f4f9)

Solutions

  1. Use the host's real LAN/public IP or an external DNS name that does not resolve to loopback/link-local
  2. If the library genuinely runs on the same network, expose it via a routable address or reverse proxy and browse that URL
  3. Never attempt to point the browser at 169.254.169.254 or other link-local targets — it is blocked by design (SSRF guard)

Example fix

// before
await zimController.browseLibrary({ url: 'http://localhost:8080/zim/' }) // 400 loopback or link-local
// after
await zimController.browseLibrary({ url: 'http://192.168.1.20:8080/zim/' })
Defensive patterns

Strategy: validation

Validate before calling

const isSafeUrl = (u: string) => {
  const h = new URL(u).hostname
  return !/^(localhost|127\.|169\.254\.|fe80:|::1$)/i.test(h)
}
if (!isSafeUrl(url)) throw new Error('URL resolves to loopback or link-local')

Type guard

const isPublicHost = (u: string): boolean => {
  try { const h = new URL(u).hostname; return !/^(localhost|127\.|0\.|169\.254\.|fe80:|::1|fc|fd)/i.test(h) } catch { return false }
}

Try / catch

try {
  await browseLibrary(url)
} catch (e) {
  if (e.status === 400 && e.message?.includes('loopback or link-local')) {
    // rewrite URL to a routable address and retry
  } else throw e
}

Prevention

When it happens

Trigger: Calling the browse-library endpoint with a URL whose hostname is localhost, 127.0.0.1, an internal name resolving to loopback, or a link-local IP such as 169.254.169.254 (cloud metadata) or fe80::1.

Common situations: Testing locally against http://localhost:8080; attempting to index an internal LAN resource by link-local address; DNS names that resolve to private/loopback ranges behind a corporate resolver; accidental metadata-endpoint access (169.254.169.254).

Related errors


AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27). Data as JSON: /api/errors/fb29e2d6a7550117. Report an issue: GitHub.