{"record":{"id":"eb0c854cb1f4f137","repo":"abhigyanpatwari/GitNexus","slug":"git-urls-must-not-include-query-strings-or-fragmen","errorCode":null,"errorMessage":"Git URLs must not include query strings or fragments","messagePattern":"Git URLs must not include query strings or fragments","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/core/net/url-guard.ts","lineNumber":29,"sourceCode":"/**\n * Validate an outbound http(s) URL to prevent SSRF.\n * Only allows https:// and http:// schemes. Blocks private/internal addresses,\n * IPv6 private ranges, cloud metadata hostnames, and numeric IP encodings.\n */\nexport function validateGitUrl(url: string): void {\n  let parsed: URL;\n  try {\n    parsed = new URL(url);\n  } catch {\n    throw new Error('Invalid URL');\n  }\n\n  if (!['https:', 'http:'].includes(parsed.protocol)) {\n    throw new Error('Only https:// and http:// git URLs are allowed');\n  }\n\n  if (parsed.search || parsed.hash) {\n    throw new Error('Git URLs must not include query strings or fragments');\n  }\n\n  const host = parsed.hostname.toLowerCase();\n\n  // Block known dangerous hostnames (cloud metadata services)\n  if (BLOCKED_HOSTNAMES.has(host)) {\n    throw new Error('Cloning from private/internal addresses is not allowed');\n  }\n\n  // Strip IPv6 brackets if present (URL parser behavior varies across Node versions)\n  let normalizedHost = host;\n  if (host.startsWith('[') && host.endsWith(']')) {\n    normalizedHost = host.slice(1, -1);\n  }\n\n  // Check if this is an IPv6 address\n  // Use manual colon detection as fallback since isIP may return 0 for some\n  // normalized IPv6 forms (e.g. ::ffff:7f00:1)","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/0d1aed942f0e8b5d3bac27519fff441aceea722d/gitnexus/src/core/net/url-guard.ts#L11-L47","documentation":"validateGitUrl forbids URLs containing a query string (?...) or fragment (#...). Such components are never meaningful for git remotes and can be used to smuggle parameters past validation, so URLs with parsed.search or parsed.hash are rejected outright.","triggerScenarios":"Calling validateGitUrl (or its callers cloneOrPull / normalizedRegistry / sanitizedHttpUrl) with a URL that includes ?query, #fragment, or tokens pasted from a web UI (e.g. 'https://host/repo?tab=readme' or trailing '#readme').","commonSituations":"Copy-pasting a repo URL straight from a browser address bar (keeps #anchor or ?params); appending access tokens as query params; template strings accidentally leaving '?ref=...' in the URL.","solutions":["Strip the query string and fragment before passing the URL (split at '?' and '#', keep the first part).","Paste the bare clone URL from the repo's 'Clone' button rather than the browser address bar.","Move credentials out of the URL — use an auth header/credential helper instead of ?token=... query params."],"exampleFix":"// before\nawait cloneOrPull(`https://github.com/acme/api?tab=readme`, dest);\n\n// after\nconst url = new URL('https://github.com/acme/api?tab=readme');\nurl.search = '';\nurl.hash = '';\nawait cloneOrPull(url.toString(), dest);","handlingStrategy":"validation","validationCode":"const cleaned = u.split('#')[0].split('?')[0];\nconst p = new URL(cleaned);\nif (p.search || p.hash) throw new Error('url still has query/fragment');","typeGuard":"const isBareGitUrl = (u: string): boolean => {\n  try { const p = new URL(u); return !p.search && !p.hash; } catch { return false; }\n};","tryCatchPattern":"try {\n  validateGitUrl(url);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('query strings or fragments')) {\n    throw new Error(`strip ?query/#fragment from ${url}`);\n  }\n  throw err;\n}","preventionTips":["Always strip search/hash from URLs taken from browser address bars.","Never pass credentials via query parameters.","Validate configured remotes once at startup with the same rules."],"tags":["url","validation","git","ssrf-guard"],"backgroundTag":"invalid-url-format","analyzedSha":"0d1aed942f0e8b5d3bac27519fff441aceea722d","analyzedAt":"2026-09-08T00:40:44.970Z","contentChangedAt":"2026-09-08T00:40:44.970Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}