{"record":{"id":"fb29e2d6a7550117","repo":"Crosstalk-Solutions/project-nomad","slug":"error-message-fb29e2","errorCode":null,"errorMessage":"${error.message}","messagePattern":"\\$\\{error\\.message\\}","errorType":"http","errorClass":null,"httpStatus":400,"severity":"warning","filePath":"admin/app/controllers/zim_controller.ts","lineNumber":222,"sourceCode":"    const payload = await request.validateUsing(idParamValidator)\n    try {\n      await this.zimService.removeCustomLibrary(payload.params.id)\n      return { message: 'Custom library removed' }\n    } catch (error) {\n      if (error.message === 'Custom library not found') {\n        return response.status(404).send({ message: error.message })\n      }\n      throw error\n    }\n  }\n\n  async browseLibrary({ request, response }: HttpContext) {\n    const payload = await request.validateUsing(browseLibraryValidator)\n    try {\n      return await this.zimService.browseLibraryUrl(payload.url)\n    } catch (error) {\n      if (error.message?.includes('loopback or link-local')) {\n        return response.status(400).send({ message: error.message })\n      }\n      return response.status(502).send({\n        message: 'Could not fetch directory listing from the provided URL',\n      })\n    }\n  }\n}\n","sourceCodeStart":204,"sourceCodeEnd":230,"githubUrl":"https://github.com/Crosstalk-Solutions/project-nomad/blob/0bd1c6f4f9888d577fe232de06ac144bb8337131/admin/app/controllers/zim_controller.ts#L204-L230","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","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)"],"exampleFix":"// before\nawait zimController.browseLibrary({ url: 'http://localhost:8080/zim/' }) // 400 loopback or link-local\n// after\nawait zimController.browseLibrary({ url: 'http://192.168.1.20:8080/zim/' })","handlingStrategy":"validation","validationCode":"const isSafeUrl = (u: string) => {\n  const h = new URL(u).hostname\n  return !/^(localhost|127\\.|169\\.254\\.|fe80:|::1$)/i.test(h)\n}\nif (!isSafeUrl(url)) throw new Error('URL resolves to loopback or link-local')","typeGuard":"const isPublicHost = (u: string): boolean => {\n  try { const h = new URL(u).hostname; return !/^(localhost|127\\.|0\\.|169\\.254\\.|fe80:|::1|fc|fd)/i.test(h) } catch { return false }\n}","tryCatchPattern":"try {\n  await browseLibrary(url)\n} catch (e) {\n  if (e.status === 400 && e.message?.includes('loopback or link-local')) {\n    // rewrite URL to a routable address and retry\n  } else throw e\n}","preventionTips":["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"],"tags":["zim","ssrf","network","url-validation","http-400"],"backgroundTag":"ssrf-url-blocked","analyzedSha":"0bd1c6f4f9888d577fe232de06ac144bb8337131","analyzedAt":"2026-08-27T05:34:15.424Z","schemaVersion":2},"datasetVersion":"2026-08-27T08:17:20.692Z"}