Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shared/utils/fetch-cache-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
47 changes: 16 additions & 31 deletions shared/utils/git-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Comment thread
gameroman marked this conversation as resolved.

/**
* No open-ended Gitea host detection for the same reason as Forgejo above.
*/
export const GITEA_HOSTS = ['gitea.com']
Comment thread
gameroman marked this conversation as resolved.

interface ProviderConfig {
id: ProviderId
/** Check if hostname matches this provider */
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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}`),
]
6 changes: 2 additions & 4 deletions shared/utils/repository-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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<GiteaRepoResponse>(
Expand Down Expand Up @@ -443,7 +441,7 @@ const radicleAdapter: ProviderAdapter = {
}

/**
* Adapter for explicit Forgejo instances.
* Adapter for exact allowlisted Forgejo instances.
*/
const forgejoAdapter: ProviderAdapter = {
links(ref) {
Expand Down
19 changes: 4 additions & 15 deletions test/nuxt/composables/use-repo-meta.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
})
})
})
Expand Down
27 changes: 9 additions & 18 deletions test/unit/shared/utils/git-providers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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',
})
Comment thread
serhalp marked this conversation as resolved.
})
})
Expand Down
Loading