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
- Use the host's real LAN/public IP or an external DNS name that does not resolve to loopback/link-local
- If the library genuinely runs on the same network, expose it via a routable address or reverse proxy and browse that URL
- 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
- Never use localhost/127.0.0.1 in library URLs
- Resolve hostnames first and reject private/link-local ranges client-side
- Remember link-local (169.254.x.x, fe80::) is blocked as an SSRF guard by design
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
- Maximum of 10 custom libraries allowed
- Could not fetch directory listing from the provided URL
- HTTP ${resp.status} from ${MANIFEST_URL}
- No internet — connect to download FDA drug data. (${msg})
- Failed to download tar file
AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27).
Data as JSON: /api/errors/fb29e2d6a7550117.
Report an issue: GitHub.