diff --git a/shared/utils/fetch-cache-config.ts b/shared/utils/fetch-cache-config.ts index 39a58791fb..ecd41cffd1 100644 --- a/shared/utils/fetch-cache-config.ts +++ b/shared/utils/fetch-cache-config.ts @@ -30,6 +30,7 @@ export const FETCH_CACHE_ALLOWED_DOMAINS = [ 'gitlab.com', // GitLab API 'api.bitbucket.org', // Bitbucket API 'codeberg.org', // Codeberg (Gitea-based) + 'gitea.com', // Gitea API 'gitee.com', // Gitee API // microcosm endpoints for atproto data CONSTELLATION_HOST, diff --git a/shared/utils/git-providers.ts b/shared/utils/git-providers.ts index 7d2d56e86d..e4ba18dfd7 100644 --- a/shared/utils/git-providers.ts +++ b/shared/utils/git-providers.ts @@ -38,6 +38,18 @@ export const GITLAB_HOSTS = [ 'framagit.org', ] +/** + * Repo URLs come from npm package metadata, so package publishers can specify any hostname. As this + * is effectively user-controlled input that can point at a malicious user-controlled server, this + * would put us at risk of Server-Side Request Forgery (SSRF). Thus we only support allowlisted hosts. + */ +export const FORGEJO_HOSTS = ['next.forgejo.org', 'try.next.forgejo.org'] + +/** + * No open-ended Gitea host detection for the same reason as Forgejo above. + */ +export const GITEA_HOSTS = ['gitea.com'] + interface ProviderConfig { id: ProviderId /** Check if hostname matches this provider */ @@ -215,14 +227,7 @@ const providers: ProviderConfig[] = [ }, { id: 'forgejo', - matchHost: host => { - // Match explicit Forgejo instances - const forgejoPatterns = [/^forgejo\./i, /\.forgejo\./i] - // Known Forgejo instances - const knownInstances = ['next.forgejo.org', 'try.next.forgejo.org'] - if (knownInstances.some(h => host === h)) return true - return forgejoPatterns.some(p => p.test(host)) - }, + matchHost: host => FORGEJO_HOSTS.includes(host), parsePath: parts => { if (parts.length < 2) return null const owner = decodeURIComponent(parts[0] ?? '').trim() @@ -244,29 +249,7 @@ const providers: ProviderConfig[] = [ }, { id: 'gitea', - matchHost: host => { - // Match common Gitea hosting patterns (Forgejo has its own adapter) - const giteaPatterns = [/^git\./i, /^gitea\./i, /^code\./i, /^src\./i, /gitea\.io$/i] - // Skip known providers (including Forgejo patterns) - const skipHosts = [ - 'github.com', - 'gitlab.com', - 'codeberg.org', - 'bitbucket.org', - 'gitee.com', - 'sr.ht', - 'git.sr.ht', - 'tangled.sh', - 'tangled.org', - 'next.forgejo.org', - 'try.next.forgejo.org', - ...GITLAB_HOSTS, - ] - if (skipHosts.some(h => host === h || host.endsWith(`.${h}`))) return false - // Skip Forgejo patterns - if (/^forgejo\./i.test(host) || /\.forgejo\./i.test(host)) return false - return giteaPatterns.some(p => p.test(host)) - }, + matchHost: host => GITEA_HOSTS.includes(host), parsePath: parts => { if (parts.length < 2) return null const owner = decodeURIComponent(parts[0] ?? '').trim() @@ -407,4 +390,6 @@ export const GIT_PROVIDER_API_ORIGINS = { export const ALL_KNOWN_GIT_API_ORIGINS: readonly string[] = [ ...Object.values(GIT_PROVIDER_API_ORIGINS), ...GITLAB_HOSTS.map(host => `https://${host}`), + ...FORGEJO_HOSTS.map(host => `https://${host}`), + ...GITEA_HOSTS.map(host => `https://${host}`), ] diff --git a/shared/utils/repository-meta.ts b/shared/utils/repository-meta.ts index 570c311eb6..4379b27025 100644 --- a/shared/utils/repository-meta.ts +++ b/shared/utils/repository-meta.ts @@ -296,7 +296,7 @@ const giteeAdapter: ProviderAdapter = { } /** - * Generic Gitea adapter for self-hosted instances. + * Adapter for exact allowlisted Gitea instances. */ const giteaAdapter: ProviderAdapter = { links(ref) { @@ -312,8 +312,6 @@ const giteaAdapter: ProviderAdapter = { async fetchMeta(cachedFetch, ref, links, options = {}) { if (!ref.host) return null - // Note: Generic Gitea instances may not be in the allowlist, - // so caching may not apply for self-hosted instances let res: GiteaRepoResponse | null = null try { const { data } = await cachedFetch( @@ -443,7 +441,7 @@ const radicleAdapter: ProviderAdapter = { } /** - * Adapter for explicit Forgejo instances. + * Adapter for exact allowlisted Forgejo instances. */ const forgejoAdapter: ProviderAdapter = { links(ref) { diff --git a/test/nuxt/composables/use-repo-meta.spec.ts b/test/nuxt/composables/use-repo-meta.spec.ts index a1e9a91a36..8e0cddf6e9 100644 --- a/test/nuxt/composables/use-repo-meta.spec.ts +++ b/test/nuxt/composables/use-repo-meta.spec.ts @@ -191,26 +191,15 @@ describe('useRepoMeta - URL parsing via repoRef', () => { }) }) - describe('Generic Gitea URLs', () => { - it('should parse git.* subdomain as Gitea', () => { - const result = parseRepoUrl('https://git.example.com/user/repo') - - expect(result).toEqual({ - provider: 'gitea', - owner: 'user', - repo: 'repo', - host: 'git.example.com', - }) - }) - - it('should parse gitea.* subdomain', () => { - const result = parseRepoUrl('https://gitea.example.org/org/project') + describe('Gitea URLs', () => { + it('should parse exact allowlisted Gitea hosts', () => { + const result = parseRepoUrl('https://gitea.com/org/project') expect(result).toEqual({ provider: 'gitea', owner: 'org', repo: 'project', - host: 'gitea.example.org', + host: 'gitea.com', }) }) }) diff --git a/test/unit/shared/utils/git-providers.spec.ts b/test/unit/shared/utils/git-providers.spec.ts index e4c6e3c5d4..9a19781267 100644 --- a/test/unit/shared/utils/git-providers.spec.ts +++ b/test/unit/shared/utils/git-providers.spec.ts @@ -325,18 +325,6 @@ describe('parseRepositoryInfo', () => { }) describe('Forgejo support', () => { - it('parses Forgejo URL from forgejo subdomain', () => { - const result = parseRepositoryInfo({ - url: 'https://forgejo.example.com/owner/repo', - }) - expect(result).toMatchObject({ - provider: 'forgejo', - owner: 'owner', - repo: 'repo', - host: 'forgejo.example.com', - }) - }) - it('parses Forgejo URL from next.forgejo.org', () => { const result = parseRepositoryInfo({ url: 'https://next.forgejo.org/forgejo/forgejo', @@ -348,16 +336,19 @@ describe('parseRepositoryInfo', () => { host: 'next.forgejo.org', }) }) + }) - it('parses Forgejo URL with .git suffix', () => { + describe('Gitea support', () => { + it('parses exact allowlisted Gitea hosts', () => { const result = parseRepositoryInfo({ - url: 'git+ssh://git@forgejo.myserver.com/user/project.git', + url: 'https://gitea.com/owner/repo', }) expect(result).toMatchObject({ - provider: 'forgejo', - owner: 'user', - repo: 'project', - host: 'forgejo.myserver.com', + provider: 'gitea', + owner: 'owner', + repo: 'repo', + host: 'gitea.com', + rawBaseUrl: 'https://gitea.com/owner/repo/raw/branch/main', }) }) })