From 79f477360605b17141964e884b740dfedc985989 Mon Sep 17 00:00:00 2001 From: Aho-Bakaa Date: Sat, 11 Jul 2026 01:45:23 +0530 Subject: [PATCH 1/3] fix(ui): allow credentials in skill repository URLs for non-GitHub hosts --- .../src/components/claude_code_plugins/helpers.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts index cab3c5cba3c..21459df55de 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts @@ -54,14 +54,16 @@ const parseRepoUrl = (raw: string): URL | null => { } if ( url.protocol !== "https:" || - url.username !== "" || - url.password !== "" || !url.hostname.includes(".") || url.hostname.startsWith("[") || IPV4_HOST_REGEX.test(url.hostname) ) { return null; } + // Allow credentials in the URL but strip them before downstream parsing + if (url.username !== "" || url.password !== "") { + url = new URL(url.pathname + url.search, url.origin); + } return url; }; From 3d59b69b8798b1a7d9d88e047bc3a47d46422d53 Mon Sep 17 00:00:00 2001 From: Aho-Bakaa Date: Sat, 11 Jul 2026 01:45:23 +0530 Subject: [PATCH 2/3] test(ui): update credential URL test to reflect new stripping behavior --- .../claude_code_plugins/helpers.test.ts | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts index 4c84db2a97d..e89853d95ac 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts @@ -524,11 +524,22 @@ describe("parseSkillSource — security boundary", () => { } }); - it("rejects URLs with embedded credentials", () => { - expect(parseSkillSource("https://user:token@gitlab.com/org/repo")).toBeNull(); - expect(parseSkillSource("https://user@gitlab.com/org/repo")).toBeNull(); - // userinfo confusion: the real host is evil.com, not github.com - expect(parseSkillSource("https://github.com@evil.com/org/repo")).toBeNull(); + it("accepts URLs with credentials and strips them", () => { + // Credentials are stripped; the repo URL parses normally afterward + expect(parseSkillSource("https://user:token@gitlab.com/org/repo")?.parsed).toEqual({ + source: "url", + url: "https://gitlab.com/org/repo", + }); + expect(parseSkillSource("https://user@gitlab.com/org/repo")?.parsed).toEqual({ + source: "url", + url: "https://gitlab.com/org/repo", + }); + // userinfo confusion: the real host is evil.com — stripping credentials + // correctly identifies evil.com as the host, NOT github.com + expect(parseSkillSource("https://github.com@evil.com/org/repo")?.parsed).toEqual({ + source: "url", + url: "https://evil.com/org/repo", + }); }); it("rejects IP-literal hosts (loopback, private, metadata, obfuscated, IPv6)", () => { From 7385867b4b145f2e25297e78e437777ee0ad6585 Mon Sep 17 00:00:00 2001 From: Aho-Bakaa Date: Sat, 11 Jul 2026 02:12:09 +0530 Subject: [PATCH 3/3] fix(ui): use in-place credential clearing to prevent host-change bypass --- .../components/claude_code_plugins/helpers.test.ts | 12 ++++++++++++ .../src/components/claude_code_plugins/helpers.ts | 9 ++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts index e89853d95ac..c6f6c62a3c4 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.test.ts @@ -554,6 +554,18 @@ describe("parseSkillSource — security boundary", () => { } }); + it("preserves validated host when stripping credentials from //path URLs", () => { + // A path starting with // is a new authority if re-parsed after stripping. + // In-place credential clearing prevents the host from changing. + // The validated host (safe.com) stays as-is — not bypassed to 127.0.0.1. + const result = parseSkillSource("https://user@safe.com//127.0.0.1/repo"); + expect(result).not.toBeNull(); + expect(result?.parsed).toEqual({ + source: "url", + url: "https://safe.com//127.0.0.1/repo", + }); + }); + it("does not grant GitHub shorthand to a look-alike host", () => { expect(parseSkillSource("https://github.com.evil.com/org/repo")?.parsed).toEqual({ source: "url", diff --git a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts index 21459df55de..fbe9f5f7c69 100644 --- a/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts +++ b/ui/litellm-dashboard/src/components/claude_code_plugins/helpers.ts @@ -36,7 +36,7 @@ const pathSegments = (url: URL): string[] => url.pathname.split("/").filter((seg /** * Validate and normalize a repository URL into a parsed URL, or null. Enforces https (rejects - * http/ssh/git/etc.), rejects embedded credentials, and requires a dotted host, so the public + * http/ssh/git/etc.), strips embedded credentials, and requires a dotted host, so the public * skill feeds never serve an insecure or credentialed clone URL. Everything downstream parses * this normalized object rather than the raw string. */ @@ -60,10 +60,9 @@ const parseRepoUrl = (raw: string): URL | null => { ) { return null; } - // Allow credentials in the URL but strip them before downstream parsing - if (url.username !== "" || url.password !== "") { - url = new URL(url.pathname + url.search, url.origin); - } + // Strip embedded credentials in-place so the validated host cannot change + url.username = ""; + url.password = ""; return url; };