{"record":{"id":"f9129b3a3a205266","repo":"ruvnet/ruflo","slug":"invalid-hostname","errorCode":null,"errorMessage":"Invalid hostname","messagePattern":"Invalid hostname","errorType":"validation","errorClass":"Error","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/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/server/isURLLocal.ts#L1-L34","documentation":"Thrown by assertValidHostname in isURLLocal.ts when the URL hostname is either empty or exceeds the RFC 1035 maximum of 253 characters. isURLLocal calls this guard only when URL.hostname is not a literal IP (checked via node:net isIP), so the value is treated as a DNS name and must satisfy DNS length limits before dns.lookup is invoked. This is the first line of defense before any SSRF check.","triggerScenarios":"Calling isURLLocal(url) or isURLStringLocal(urlStr) with a URL whose hostname is '' (e.g. 'http:///path' or 'file:///path') or longer than 253 bytes (e.g. a very long punycoded IDN, or a data: URL coerced into a URL object). Also reachable when a fetch-url endpoint or conversation request supplies a malformed host.","commonSituations":"Bugs that build URLs from untrusted user input without validating the host; IDN/Unicode hostnames that expand past 253 bytes once punycoded; copy-pasted URLs with extra path data mistakenly placed in the host slot; tests that pass synthetic URLs like new URL('http://').","solutions":["Validate or sanitize the hostname (length 1-253, RFC 1035 labels) before calling isURLLocal.","If the host can legitimately be empty, short-circuit before calling isURLLocal and treat it as invalid input rather than relying on the throw.","For IDN hostnames, normalize to punycode (url.hostname already returns ASCII via WHATWG URL) and confirm the encoded length is within 253."],"exampleFix":"// before\nconst local = await isURLLocal(new URL(userInput));\n\n// after\nconst u = new URL(userInput);\nif (!u.hostname || u.hostname.length > 253) {\n  return { valid: false, reason: 'invalid-host' };\n}\nconst local = await isURLLocal(u);","handlingStrategy":"validation","validationCode":"import { isIP } from \"node:net\";\n\nfunction isValidDnsHostname(host: string): boolean {\n  if (!host || host.length > 253) return false;\n  if (isIP(host)) return true; // IP literals skip assertValidHostname\n  const labels = host.split(\".\");\n  for (const l of labels) {\n    if (!l || l.length > 63) return false;\n  }\n  return true;\n}\n\nif (!isValidDnsHostname(url.hostname)) return { valid: false };","typeGuard":"function isPlausibleHostname(host: string): host is string {\n  return typeof host === \"string\" && host.length > 0 && host.length <= 253 && !host.includes(\"..\");\n}","tryCatchPattern":"try {\n  const local = await isURLLocal(url);\n} catch (e) {\n  if (e instanceof Error && e.message === \"Invalid hostname\") {\n    // reject the input; do not fall through to default-local\n    return { valid: false, reason: \"invalid-hostname\" };\n  }\n  throw e;\n}","preventionTips":["Always construct URLs via new URL(...) so hostname is parsed by WHATWG rules before validation.","Reject empty hostnames at the input boundary instead of relying on assertValidHostname to throw.","Track the 253-char limit in any code that synthesizes hostnames (e.g. generated subdomains)."],"tags":["validation","ssrf","hostname","url","dns","rfc1035"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}