This commit is contained in:
Anmol 2026-08-27 17:52:59 -05:00 committed by GitHub
commit 5f07fa21c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 32 additions and 8 deletions

View file

@ -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)", () => {
@ -543,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",

View file

@ -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.
*/
@ -54,14 +54,15 @@ 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;
}
// Strip embedded credentials in-place so the validated host cannot change
url.username = "";
url.password = "";
return url;
};