Skip to content

Commit b924e46

Browse files
committed
fix: properly validate GitHub repository URLs to prevent URL substring attacks
1 parent d4ca58d commit b924e46

File tree

2 files changed

+49
-2
lines changed

2 files changed

+49
-2
lines changed

src/utils/__tests__/git.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,12 +915,34 @@ describe("isGitHubRepository", () => {
915915
expect(isGitHubRepository("https://GITHUB.COM/user/repo.git")).toBe(true)
916916
})
917917

918+
it("should return true for GitHub subdomains", () => {
919+
expect(isGitHubRepository("https://gist.github.com/user/repo")).toBe(true)
920+
expect(isGitHubRepository("https://api.github.com/repos/user/repo")).toBe(true)
921+
expect(isGitHubRepository("[email protected]:user/repo.git")).toBe(true)
922+
})
923+
918924
it("should return false for non-GitHub URLs", () => {
919925
expect(isGitHubRepository("https://gitlab.com/user/repo.git")).toBe(false)
920926
expect(isGitHubRepository("https://bitbucket.org/user/repo.git")).toBe(false)
921927
expect(isGitHubRepository("[email protected]:user/repo.git")).toBe(false)
922928
})
923929

930+
it("should return false for malicious URLs with github.com in hostname", () => {
931+
// Security: These URLs have "github.com" as part of the hostname but are not GitHub
932+
expect(isGitHubRepository("https://malicious-github.com/user/repo.git")).toBe(false)
933+
expect(isGitHubRepository("https://github.com.evil.com/user/repo.git")).toBe(false)
934+
expect(isGitHubRepository("https://fake-github.com/user/repo.git")).toBe(false)
935+
expect(isGitHubRepository("[email protected]:user/repo.git")).toBe(false)
936+
expect(isGitHubRepository("ssh://[email protected]/user/repo.git")).toBe(false)
937+
})
938+
939+
it("should return false for URLs with github.com in the path", () => {
940+
// Security: These URLs have "github.com" in the path but not as the hostname
941+
expect(isGitHubRepository("https://evil.com/github.com/malicious/repo.git")).toBe(false)
942+
expect(isGitHubRepository("https://attacker.com/fake/github.com/path")).toBe(false)
943+
expect(isGitHubRepository("[email protected]:github.com/user/repo.git")).toBe(false)
944+
})
945+
924946
it("should return false for undefined or empty URLs", () => {
925947
expect(isGitHubRepository(undefined)).toBe(false)
926948
expect(isGitHubRepository("")).toBe(false)

src/utils/git.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,33 @@ export function isGitHubRepository(repositoryUrl?: string): boolean {
222222
}
223223

224224
try {
225-
// Check if the URL contains github.com
226-
return repositoryUrl.toLowerCase().includes("github.com")
225+
const url = repositoryUrl.toLowerCase().trim()
226+
227+
// Try to parse as HTTPS/HTTP URL
228+
if (url.startsWith("https://") || url.startsWith("http://")) {
229+
const parsed = new URL(url)
230+
return parsed.hostname === "github.com" || parsed.hostname.endsWith(".github.com")
231+
}
232+
233+
// Handle SSH format: git@github.com:user/repo.git
234+
if (url.startsWith("git@")) {
235+
const match = url.match(/^git@([^:]+):/)
236+
if (match && match[1]) {
237+
const host = match[1]
238+
return host === "github.com" || host.endsWith(".github.com")
239+
}
240+
}
241+
242+
// Handle SSH with protocol: ssh://git@github.com/user/repo.git
243+
if (url.startsWith("ssh://")) {
244+
const match = url.match(/^ssh:\/\/(?:git@)?([^\/]+)/)
245+
if (match && match[1]) {
246+
const host = match[1]
247+
return host === "github.com" || host.endsWith(".github.com")
248+
}
249+
}
250+
251+
return false
227252
} catch {
228253
return false
229254
}

0 commit comments

Comments
 (0)