Skip to content

Commit 481b30d

Browse files
fix: git agnostic safe url
1 parent a41ae27 commit 481b30d

3 files changed

Lines changed: 36 additions & 23 deletions

File tree

src/openapi/definitions.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,7 +940,7 @@ replicas:
940940
default: 1
941941
repoUrl:
942942
description: Path to a remote git repo without protocol. Will use https to access.
943-
pattern: '^(https://)?(github\.com|gitlab\.com|[^/\s]+\.gitea[^/\s]*)/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:\.git)?$'
943+
pattern: '^(?:(?:https://)?[A-Za-z0-9.-]+\.[A-Za-z]{2,}|git@[A-Za-z0-9.-]+\.[A-Za-z]{2,}:)/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+(?:\.git)?$'
944944
type: string
945945
x-message: a valid git repo URL
946946
example: github.com/example/repo

src/utils/codeRepoUtils.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,29 @@ describe('codeRepoUtils', () => {
134134
expect(normalizeRepoUrl(repoUrl, false, false)).toBeNull()
135135
})
136136

137+
it.each([
138+
['https://github.com/example/repo', 'https://github.com/example/repo.git'],
139+
['github.com/example/repo', 'https://github.com/example/repo.git'],
140+
['git@github.com:example/repo.git', 'https://github.com/example/repo.git'],
141+
[
142+
'https://gitlab.example.com/platform/backend/my-repo',
143+
'https://gitlab.example.com/platform/backend/my-repo.git',
144+
],
145+
['gitlab.example.com/platform/backend/my-repo', 'https://gitlab.example.com/platform/backend/my-repo.git'],
146+
[
147+
'git@gitlab.example.com:platform/backend/my-repo.git',
148+
'https://gitlab.example.com/platform/backend/my-repo.git',
149+
],
150+
])('should normalize valid repository URL: %s', (input, expected) => {
151+
expect(normalizeRepoUrl(input, false, false)).toEqual(expected)
152+
})
153+
154+
it('should preserve SSH format for private SSH repositories', () => {
155+
const result = normalizeRepoUrl('git@gitlab.example.com:platform/backend/my-repo.git', true, true)
156+
157+
expect(result).toEqual('git@gitlab.example.com:platform/backend/my-repo.git')
158+
})
159+
137160
it('should return null for invalid URL', () => {
138161
const result = normalizeRepoUrl('invalid-url', false, false)
139162
expect(result).toBeNull()

src/utils/codeRepoUtils.ts

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -44,57 +44,47 @@ export async function getGiteaRepoUrls(adminUsername, adminPassword, orgName, do
4444
}
4545
}
4646

47-
const ALLOWED_REPO_HOSTS = ['github.com', 'gitlab.com']
48-
49-
const SAFE_REPO_PATH_REGEX = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/
47+
const SAFE_HOST_REGEX = /^[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/
48+
const SAFE_REPO_PATH_REGEX = /^[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+$/
5049

5150
export function normalizeRepoUrl(inputUrl: string, isPrivate: boolean, isSSH: boolean): string | null {
5251
try {
5352
const cleanUrl = inputUrl.trim().replace(/\/$/, '')
5453

5554
let hostname: string
56-
let owner: string
57-
let repoName: string
55+
let repoPath: string
5856

5957
if (cleanUrl.startsWith('git@')) {
6058
const match = cleanUrl
6159
.replace(/\.git$/, '')
62-
.match(/^git@(github\.com|gitlab\.com):([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)$/)
60+
.match(/^git@([A-Za-z0-9.-]+\.[A-Za-z]{2,}):([A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+)$/)
6361

6462
if (!match) return null
6563

6664
hostname = match[1]
67-
owner = match[2]
68-
repoName = match[3]
65+
repoPath = match[2]
6966
} else {
7067
const urlToParse = /^[a-z][a-z0-9+.-]*:/i.test(cleanUrl) ? cleanUrl : `https://${cleanUrl}`
7168

7269
const parsed = new URL(urlToParse)
7370

7471
if (parsed.protocol !== 'https:') return null
72+
if (!SAFE_HOST_REGEX.test(parsed.hostname)) return null
7573

76-
hostname = parsed.hostname
77-
if (!ALLOWED_REPO_HOSTS.includes(hostname)) return null
74+
repoPath = parsed.pathname.replace(/\.git$/, '').replace(/^\/|\/$/g, '')
7875

79-
const parts = parsed.pathname
80-
.replace(/\.git$/, '')
81-
.split('/')
82-
.filter(Boolean)
83-
if (parts.length !== 2) return null
76+
if (!SAFE_REPO_PATH_REGEX.test(repoPath)) return null
8477

85-
owner = parts[0]
86-
repoName = parts[1]
87-
88-
if (!SAFE_REPO_PATH_REGEX.test(`${owner}/${repoName}`)) return null
78+
hostname = parsed.hostname
8979
}
9080

91-
const repoWithGitSuffix = `${repoName}.git`
81+
const repoWithGitSuffix = `${repoPath}.git`
9282

9383
if (isPrivate && isSSH) {
94-
return `git@${hostname}:${owner}/${repoWithGitSuffix}`
84+
return `git@${hostname}:${repoWithGitSuffix}`
9585
}
9686

97-
return `https://${hostname}/${owner}/${repoWithGitSuffix}`
87+
return `https://${hostname}/${repoWithGitSuffix}`
9888
} catch {
9989
return null
10090
}

0 commit comments

Comments
 (0)