{"record":{"id":"3ab43620e9eda8b5","repo":"ComposioHQ/composio","slug":"refusing-to-fetch-a-malformed-url","errorCode":null,"errorMessage":"Refusing to fetch a malformed URL","messagePattern":"Refusing to fetch a malformed URL","errorType":"exception","errorClass":"ComposioBlockedInternalUrlError","httpStatus":null,"severity":"error","filePath":"ts/packages/core/src/utils/ssrfGuard.node.ts","lineNumber":176,"sourceCode":"  if (family === 6) return isBlockedIpv6(ip);\n  return true;\n};\n\n/**\n * Validate a single URL: it must be http(s), its host must resolve, and every\n * resolved address must be publicly routable. Throws\n * {@link ComposioBlockedInternalUrlError} otherwise.\n *\n * @returns the validated addresses to connect to, in resolver order. Callers\n * must connect to *those* rather than let the client resolve the hostname\n * again; see {@link ssrfSafeFetch}.\n */\nexport const assertSafeFetchTarget = async (rawUrl: string): Promise<string[]> => {\n  let url: URL;\n  try {\n    url = new URL(rawUrl);\n  } catch {\n    throw new ComposioBlockedInternalUrlError('Refusing to fetch a malformed URL', { url: rawUrl });\n  }\n\n  if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n    throw new ComposioBlockedInternalUrlError(\n      `Refusing to fetch a non-http(s) URL (scheme \"${url.protocol}\")`,\n      { url: rawUrl }\n    );\n  }\n\n  const host = url.hostname.replace(/^\\[|\\]$/g, '');\n\n  let resolved: Array<{ address: string }>;\n  try {\n    resolved = await lookup(host, { all: true, verbatim: true });\n  } catch {\n    throw new ComposioBlockedInternalUrlError(`Could not resolve host \"${host}\"`, { url: rawUrl });\n  }\n","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/ComposioHQ/composio/blob/64b1b85502b1beeb2379e6c9e8bf1104504fa637/ts/packages/core/src/utils/ssrfGuard.node.ts#L158-L194","documentation":"Part of the SSRF guard for URL-based file uploads: assertSafeFetchTarget parses the target URL and refuses to proceed if it cannot be parsed as a URL at all. Failing closed prevents accidental fetches of malformed targets.","triggerScenarios":"Passing a URL to a URL-upload API (e.g. uploading a file by URL) that is not parseable by new URL() — missing scheme ('example.com/file'), stray whitespace/characters, or a relative path.","commonSituations":"LLM-generated URLs without a scheme; user input pasted with leading spaces; concatenating strings that produce '/path' instead of 'https://host/path'.","solutions":["Normalize the input to an absolute URL with an explicit https:// scheme before passing it","Trim whitespace and URL-encode spaces in the path","Validate with new URL(url) in your own code before calling the upload API"],"exampleFix":"// before\nawait upload.uploadFileAtUrl('example.com/files/report.pdf');\n\n// after\nawait upload.uploadFileAtUrl('https://example.com/files/report.pdf');","handlingStrategy":"validation","validationCode":"const isAbsoluteHttpUrl = (u: string) => { try { const x = new URL(u); return x.protocol === 'https:'; } catch { return false; } };","typeGuard":"const isParsableUrl = (u: unknown): u is string =>\n  typeof u === 'string' && u.trim().length > 0 && (() => { try { new URL(u); return true; } catch { return false; } })();","tryCatchPattern":"try {\n  await upload.uploadFileAtUrl(url);\n} catch (e) {\n  if (e instanceof ComposioBlockedInternalUrlError && /malformed/.test(e.message)) {\n    url = new URL(url.trim()).toString(); // then retry with normalized URL\n  }\n}","preventionTips":["Normalize user/LLM input with new URL() and require https before calling upload APIs","Trim and encode URL strings at the input boundary","Reject scheme-less URLs in your own validation layer"],"tags":["ssrf","url-validation","file-upload","security"],"backgroundTag":"invalid-url-format","analyzedSha":"64b1b85502b1beeb2379e6c9e8bf1104504fa637","analyzedAt":"2026-08-28T15:39:33.623Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}