Skip to content

Commit dfdf999

Browse files
committed
fix: security issue
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 9b9573a commit dfdf999

4 files changed

Lines changed: 116 additions & 32 deletions

File tree

services/apps/packages_worker/src/maven/__tests__/registry.test.ts

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { describe, expect, it } from 'vitest'
22

33
import {
44
GRADLE_PLUGIN_PORTAL_BASE_URL,
5+
JITPACK_BASE_URL,
56
MAVEN_CENTRAL_BASE_URL,
7+
isJitpackNamespace,
8+
jitpackPageUrl,
69
resolveRegistryBaseUrl,
710
resolveRegistryPageUrl,
811
resolveRegistryPageUrlFromBase,
@@ -150,19 +153,19 @@ describe('resolveRegistryBaseUrl', () => {
150153
)
151154
})
152155

153-
it('returns JitPack for io.github namespace', () => {
154-
expect(resolveRegistryBaseUrl('io.github.resilience4j')).toBe('https://jitpack.io')
156+
it('returns Maven Central for io.github namespace (JitPack is a fallback, not primary)', () => {
157+
expect(resolveRegistryBaseUrl('io.github.resilience4j')).toBe(MAVEN_CENTRAL_BASE_URL)
155158
})
156159

157-
it('returns JitPack for bare io.github (exact match)', () => {
158-
expect(resolveRegistryBaseUrl('io.github')).toBe('https://jitpack.io')
160+
it('returns Maven Central for bare io.github', () => {
161+
expect(resolveRegistryBaseUrl('io.github')).toBe(MAVEN_CENTRAL_BASE_URL)
159162
})
160163

161-
it('returns JitPack for com.github namespace', () => {
162-
expect(resolveRegistryBaseUrl('com.github.ben-manes')).toBe('https://jitpack.io')
164+
it('returns Maven Central for com.github namespace (JitPack is a fallback, not primary)', () => {
165+
expect(resolveRegistryBaseUrl('com.github.ben-manes')).toBe(MAVEN_CENTRAL_BASE_URL)
163166
})
164167

165-
it('does not match io.githubfoo — prefix boundary must end at a dot', () => {
168+
it('returns Maven Central for io.githubfoo (no dot boundary)', () => {
166169
expect(resolveRegistryBaseUrl('io.githubfoo')).toBe(MAVEN_CENTRAL_BASE_URL)
167170
})
168171
})
@@ -192,21 +195,17 @@ describe('resolveRegistryPageUrl', () => {
192195
)
193196
})
194197

195-
it('returns JitPack browse URL for io.github', () => {
198+
it('returns Maven Central URL for io.github (Central is primary)', () => {
196199
expect(resolveRegistryPageUrl('io.github.resilience4j', 'resilience4j-core')).toBe(
197-
'https://jitpack.io/#resilience4j/resilience4j-core',
200+
'https://central.sonatype.com/artifact/io.github.resilience4j/resilience4j-core',
198201
)
199202
})
200203

201-
it('returns JitPack browse URL for com.github', () => {
204+
it('returns Maven Central URL for com.github (Central is primary)', () => {
202205
expect(resolveRegistryPageUrl('com.github.ben-manes', 'caffeine')).toBe(
203-
'https://jitpack.io/#ben-manes/caffeine',
206+
'https://central.sonatype.com/artifact/com.github.ben-manes/caffeine',
204207
)
205208
})
206-
207-
it('returns JitPack browse URL for bare io.github prefix without leaking the prefix into the path', () => {
208-
expect(resolveRegistryPageUrl('io.github', 'some-lib')).toBe('https://jitpack.io/#/some-lib')
209-
})
210209
})
211210

212211
describe('resolveRegistryPageUrlFromBase', () => {
@@ -236,4 +235,55 @@ describe('resolveRegistryPageUrlFromBase', () => {
236235
),
237236
).toBe('https://plugins.gradle.org/m2/io/github/trueangle/gradle-plugin/')
238237
})
238+
239+
it('returns JitPack browse URL when resolvedBaseUrl is JitPack', () => {
240+
expect(
241+
resolveRegistryPageUrlFromBase('io.github.trueangle', 'gradle-plugin', JITPACK_BASE_URL),
242+
).toBe('https://jitpack.io/#trueangle/gradle-plugin')
243+
})
244+
})
245+
246+
describe('isJitpackNamespace', () => {
247+
it('returns true for io.github.* packages', () => {
248+
expect(isJitpackNamespace('io.github.resilience4j')).toBe(true)
249+
})
250+
251+
it('returns true for bare io.github', () => {
252+
expect(isJitpackNamespace('io.github')).toBe(true)
253+
})
254+
255+
it('returns true for com.github.* packages', () => {
256+
expect(isJitpackNamespace('com.github.ben-manes')).toBe(true)
257+
})
258+
259+
it('returns true for bare com.github', () => {
260+
expect(isJitpackNamespace('com.github')).toBe(true)
261+
})
262+
263+
it('returns false for io.githubfoo (no dot boundary)', () => {
264+
expect(isJitpackNamespace('io.githubfoo')).toBe(false)
265+
})
266+
267+
it('returns false for unrelated namespaces', () => {
268+
expect(isJitpackNamespace('org.apache.commons')).toBe(false)
269+
expect(isJitpackNamespace('io.jenkins.plugins')).toBe(false)
270+
})
271+
})
272+
273+
describe('jitpackPageUrl', () => {
274+
it('strips io.github. prefix from groupId', () => {
275+
expect(jitpackPageUrl('io.github.resilience4j', 'resilience4j-core')).toBe(
276+
'https://jitpack.io/#resilience4j/resilience4j-core',
277+
)
278+
})
279+
280+
it('strips com.github. prefix from groupId', () => {
281+
expect(jitpackPageUrl('com.github.ben-manes', 'caffeine')).toBe(
282+
'https://jitpack.io/#ben-manes/caffeine',
283+
)
284+
})
285+
286+
it('handles bare io.github with no sub-namespace', () => {
287+
expect(jitpackPageUrl('io.github', 'some-lib')).toBe('https://jitpack.io/#/some-lib')
288+
})
239289
})

services/apps/packages_worker/src/maven/metadata.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import axios from 'axios'
1212
import { XMLParser } from 'fast-xml-parser'
1313

1414
import { isPrerelease } from './normalize'
15-
import { resolveRegistryBaseUrl } from './registry'
15+
import { JITPACK_BASE_URL, resolveRegistryBaseUrl } from './registry'
1616

1717
const REQUEST_TIMEOUT_MS = 10_000
1818
const MAX_RETRIES = 3
@@ -115,7 +115,7 @@ export async function resolveVersionsList(
115115
// that don't exist as public builds. Scoped to JitPack only — other
116116
// registries may return 401 for auth failures, which should not be
117117
// silently treated as a missing package.
118-
if (status === 404 || (status === 401 && url.includes('jitpack.io')))
118+
if (status === 404 || (status === 401 && url.startsWith(JITPACK_BASE_URL + '/')))
119119
return { kind: 'NOT_FOUND' }
120120
// 429 = explicit rate limit, 403 = CDN throttle (Maven Central uses both)
121121
if ((status === 429 || status === 403) && attempt < MAX_RETRIES) {

services/apps/packages_worker/src/maven/registry.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,17 @@
99
*
1010
* All these repos expose the standard Maven repository layout, so the same
1111
* metadata.xml / POM fetch logic works — only the base URL changes.
12+
*
13+
* JitPack (io.github.*, com.github.*) is NOT listed here as a primary registry
14+
* because many well-known artifacts under those prefixes are published on Maven
15+
* Central (e.g. io.github.resilience4j, com.github.ben-manes:caffeine). Maven
16+
* Central is tried first; JitPack is used only as a fallback when Central returns
17+
* NOT_FOUND (see the enrichment loop).
1218
*/
1319

1420
export const MAVEN_CENTRAL_BASE_URL = 'https://repo1.maven.org/maven2'
1521
export const GRADLE_PLUGIN_PORTAL_BASE_URL = 'https://plugins.gradle.org/m2'
22+
export const JITPACK_BASE_URL = 'https://jitpack.io'
1623

1724
interface RegistryEntry {
1825
prefix: string
@@ -100,21 +107,6 @@ const ALTERNATIVE_REGISTRIES: RegistryEntry[] = [
100107
baseUrl: GRADLE_PLUGIN_PORTAL_BASE_URL,
101108
pageUrl: (g, a) => `${GRADLE_PLUGIN_PORTAL_BASE_URL}/${g.replace(/\./g, '/')}/${a}/`,
102109
},
103-
// JitPack builds and serves artifacts directly from GitHub/GitLab/Bitbucket source repos.
104-
// The io.github.<username> and com.github.<username> groupId conventions are JitPack-specific
105-
// and will never resolve on Maven Central.
106-
{
107-
prefix: 'io.github',
108-
baseUrl: 'https://jitpack.io',
109-
pageUrl: (g, a) =>
110-
`https://jitpack.io/#${g.startsWith('io.github.') ? g.slice('io.github.'.length) : ''}/${a}`,
111-
},
112-
{
113-
prefix: 'com.github',
114-
baseUrl: 'https://jitpack.io',
115-
pageUrl: (g, a) =>
116-
`https://jitpack.io/#${g.startsWith('com.github.') ? g.slice('com.github.'.length) : ''}/${a}`,
117-
},
118110
{
119111
prefix: 'com.cloudbees.plugins',
120112
baseUrl: 'https://repo.jenkins-ci.org/public',
@@ -182,6 +174,30 @@ export function isAlternativeRegistry(groupId: string): boolean {
182174
return findEntry(groupId) !== undefined
183175
}
184176

177+
/**
178+
* Returns true for io.github.* and com.github.* groupIds — the JitPack namespace
179+
* convention. Maven Central is tried first for these; JitPack is the fallback.
180+
*/
181+
export function isJitpackNamespace(groupId: string): boolean {
182+
return (
183+
groupId === 'io.github' ||
184+
groupId.startsWith('io.github.') ||
185+
groupId === 'com.github' ||
186+
groupId.startsWith('com.github.')
187+
)
188+
}
189+
190+
/** Returns a human-browsable JitPack URL for an io.github.* or com.github.* artifact. */
191+
export function jitpackPageUrl(groupId: string, artifactId: string): string {
192+
if (groupId.startsWith('io.github.')) {
193+
return `${JITPACK_BASE_URL}/#${groupId.slice('io.github.'.length)}/${artifactId}`
194+
}
195+
if (groupId.startsWith('com.github.')) {
196+
return `${JITPACK_BASE_URL}/#${groupId.slice('com.github.'.length)}/${artifactId}`
197+
}
198+
return `${JITPACK_BASE_URL}/#/${artifactId}`
199+
}
200+
185201
/**
186202
* Returns a human-browsable URL for the given artifact in its authoritative registry.
187203
*/
@@ -211,5 +227,8 @@ export function resolveRegistryPageUrlFromBase(
211227
if (resolvedBaseUrl === GRADLE_PLUGIN_PORTAL_BASE_URL) {
212228
return `${GRADLE_PLUGIN_PORTAL_BASE_URL}/${groupId.replace(/\./g, '/')}/${artifactId}/`
213229
}
230+
if (resolvedBaseUrl === JITPACK_BASE_URL) {
231+
return jitpackPageUrl(groupId, artifactId)
232+
}
214233
return resolveRegistryPageUrl(groupId, artifactId)
215234
}

services/apps/packages_worker/src/maven/runMavenEnrichmentLoop.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import { isMavenFetchError, resolveVersionsList } from './metadata'
2020
import { isPrerelease, parseRepoUrl } from './normalize'
2121
import {
2222
GRADLE_PLUGIN_PORTAL_BASE_URL,
23+
JITPACK_BASE_URL,
2324
MAVEN_CENTRAL_BASE_URL,
2425
isAlternativeRegistry,
26+
isJitpackNamespace,
2527
resolveRegistryBaseUrl,
2628
resolveRegistryPageUrl,
2729
resolveRegistryPageUrlFromBase,
@@ -142,6 +144,19 @@ async function processCriticalPackage(qx: QueryExecutor, pkg: PackageRow, forceF
142144
}
143145
}
144146

147+
// Second fallback: JitPack for io.github.* / com.github.* packages not found on
148+
// Maven Central. These namespaces are also used by well-known Central artifacts
149+
// (e.g. resilience4j, caffeine), so Central is tried first; JitPack is the fallback
150+
// for packages that genuinely live only on JitPack.
151+
if (isMavenFetchError(metadata) && metadata.kind === 'NOT_FOUND' && isJitpackNamespace(groupId)) {
152+
const jitpackMetadata = await resolveVersionsList(groupId, artifactId, JITPACK_BASE_URL)
153+
if (!isMavenFetchError(jitpackMetadata)) {
154+
log.info({ groupId, artifactId }, 'Not found on Maven Central — resolved via JitPack fallback')
155+
baseUrl = JITPACK_BASE_URL
156+
metadata = jitpackMetadata
157+
}
158+
}
159+
145160
// Third fallback: Gradle Plugin Portal. Plugins can be published under any groupId
146161
// (not just gradle.plugin.*), so this is worth trying for all NOT_FOUND packages.
147162
// Skip when GPP is already the primary registry (gradle.plugin.*) to avoid a

0 commit comments

Comments
 (0)