{"record":{"id":"f9129b3a3a205266","repo":"ruvnet/ruflo","slug":"invalid-hostname","errorCode":null,"errorMessage":"Invalid hostname","messagePattern":"Invalid hostname","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"ruflo/src/ruvocal/src/lib/server/isURLLocal.ts","lineNumber":16,"sourceCode":"import { Address6, Address4 } from \"ip-address\";\nimport dns from \"node:dns\";\nimport { isIP } from \"node:net\";\n\nconst dnsLookup = (hostname: string): Promise<{ address: string; family: number }> => {\n\treturn new Promise((resolve, reject) => {\n\t\tdns.lookup(hostname, (err, address, family) => {\n\t\t\tif (err) return reject(err);\n\t\t\tresolve({ address, family });\n\t\t});\n\t});\n};\n\nfunction assertValidHostname(hostname: string): void {\n\tif (!hostname || hostname.length > 253) {\n\t\tthrow new Error(\"Invalid hostname\");\n\t}\n\n\tconst labels = hostname.split(\".\");\n\n\tfor (const label of labels) {\n\t\tif (!label || label.length > 63) {\n\t\t\tthrow new Error(\"Invalid hostname\");\n\t\t}\n\n\t\tif (!/^[A-Za-z0-9-]+$/.test(label)) {\n\t\t\tthrow new Error(\"Invalid hostname\");\n\t\t}\n\n\t\tif (label.startsWith(\"-\") || label.endsWith(\"-\")) {\n\t\t\tthrow new Error(\"Invalid hostname\");\n\t\t}\n\t}\n}","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/ruflo/src/ruvocal/src/lib/server/isURLLocal.ts#L1-L34","documentation":"isURLLocal() performs an SSRF check on URLs before the server fetches them: non-IP hostnames go through assertValidHostname (isURLLocal.ts:16). The first rule rejects empty hostnames and hostnames longer than 253 characters (the RFC 1035 limit) before any DNS lookup, throwing \"Invalid hostname\".","triggerScenarios":"Calling isURLLocal(new URL(u)) with a URL whose hostname is empty (\"http:///path\", \"http://:8080/x\") or a DNS name exceeding 253 chars (deeply nested junk subdomains) — typical of user-supplied links passed to the fetch-url/link-preview endpoints.","commonSituations":"Malformed user input reaching the server unvalidated; test fixtures with degenerate URLs; abuse probes sending oversized hostnames to scan the fetch endpoint.","solutions":["Validate and normalize user URLs at the API boundary before calling isURLLocal (try new URL(), then check hostname length)","Return a 400 for unparseable/oversized URLs instead of letting the internal guard throw","Trim/limit hostname length (<253) in the same pre-check"],"exampleFix":"// before\nconst isLocal = await isURLLocal(new URL(userUrl)); // throws: Invalid hostname for \"http:///x\"\n\n// after\nlet parsed: URL;\ntry {\n\tparsed = new URL(userUrl);\n} catch {\n\tthrow error(400, \"Invalid URL\");\n}\nif (!parsed.hostname || parsed.hostname.length > 253) {\n\tthrow error(400, \"Invalid hostname\");\n}\nconst isLocal = await isURLLocal(parsed);","handlingStrategy":"validation","validationCode":"let u: URL;\ntry {\n\tu = new URL(userUrl);\n} catch {\n\tthrow error(400, \"Invalid URL\");\n}\nif (!u.hostname || u.hostname.length > 253) throw error(400, \"Invalid hostname\");\nconst isLocal = await isURLLocal(u);","typeGuard":"function hasPlausibleHostname(u: URL): boolean {\n\treturn u.hostname.length > 0 && u.hostname.length <= 253;\n}","tryCatchPattern":"try {\n\tawait isURLLocal(u);\n} catch (e) {\n\tif (e instanceof Error && e.message === \"Invalid hostname\") throw error(400, \"Bad URL\");\n\tthrow e;\n}","preventionTips":["Validate URL shape at the API boundary (parse + hostname length) before any fetch-url handling","Rate-limit and size-cap user-supplied URL fields so 253+ char hostnames never reach the guard","Treat guard throws as 400s, not 500s, in route error handlers"],"tags":["ssrf","url-validation","hostname","dns","user-input"],"backgroundTag":"invalid-hostname","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}