Summary
I found that Gitea's "add repository to team" API endpoint lets any ordinary team member grant their entire team access to a repository they personally have admin rights on, completely bypassing the organization's RepoAdminChangeTeamAccess setting - a setting whose entire purpose is to prevent exactly this. There's a second, equivalent endpoint for the same action that correctly enforces this setting, which is how I noticed the gap.
Details
PUT /api/v1/teams/{id}/repos/{owner}/{repo} is implemented by AddTeamRepository in routers/api/v1/org/team.go. It's registered in routers/api/v1/api.go behind reqTeamMembership(), which - per its own doc comment "user should be a team member, or a site admin" - passes for any ordinary member of the team in question (verified via IsTeamMember), not just a team admin or org owner. Inside the handler, the only authorization check is:
if access, err := access_model.AccessLevel(ctx, ctx.Doer, repo); err != nil {
...
} else if access < perm.AccessModeAdmin {
ctx.APIError(http.StatusForbidden, "Must have admin-level access to the repository")
}
This checks that the caller has admin access on the repo being attached - but says nothing about whether the org allows repo admins to attach teams in the first place. There's a dedicated setting for exactly that, RepoAdminChangeTeamAccess (default false, defined in models/user/user.go), and Gitea does enforce it - just not here. The other endpoint that links a team to a repo, PUT /api/v1/repos/{owner}/{repo}/teams/{team} (changeRepoTeam in routers/api/v1/repo/teams.go), does it correctly:
if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.Permission.IsOwner() {
ctx.APIError(http.StatusForbidden, "user is nor repo admin nor owner")
}
So with the default setting, only the org owner can link a team to a repo through that endpoint - but through AddTeamRepository, anyone who is both a repo admin (a very common role to hand out to trusted external contributors on a single private repo) and an ordinary member of some other team can link that entire team to the repo, silently granting every current and future member of that team access to it. I confirmed this same code path is present in the current latest release, v1.26.4.
PoC
Prerequisites: an account that (a) has Admin-level access on a target private repository org/private-repo - a level of access an org owner might hand to a trusted contributor for just that one repo - and (b) is an ordinary (non-admin) member of some team in the same org, say team ID 5. The org has RepoAdminChangeTeamAccess left at its default (false).
curl -s -X PUT -H "Authorization: token $ATTACKER_TOKEN" \
"https://TARGET_HOST/api/v1/teams/5/repos/org/private-repo"
# Expected: HTTP 204 No Content - every current and future member of team 5
# now has whatever access level team 5 grants (up to Write/Admin) to
# org/private-repo, despite RepoAdminChangeTeamAccess being disabled.
# Compare: the equivalent, correctly-guarded endpoint refuses the same actor:
curl -s -X PUT -H "Authorization: token $ATTACKER_TOKEN" \
"https://TARGET_HOST/api/v1/repos/org/private-repo/teams/some-team-name"
# Expected: HTTP 403 {"message":"user is nor repo admin nor owner"}
Impact
This is a broken access control / privilege escalation issue. It lets a non-owner who holds admin rights on a single repository silently expand access to that repository to an entire team's worth of other users, bypassing an org-level control specifically designed to prevent that. On orgs that grant per-repo admin access to external contributors (a common pattern), this could expose private repository code and history to users the org owner never intended to grant access to.
Fix
AddTeamRepository (and its RemoveTeamRepository counterpart, which likely has the same gap) should enforce the same check as changeRepoTeam: require ctx.Org.Organization.RepoAdminChangeTeamAccess or org-owner status before proceeding, instead of only checking the caller's access level on the target repo.
If possible, please apply for a CVE number when publishing. I would greatly appreciate it.
Summary
I found that Gitea's "add repository to team" API endpoint lets any ordinary team member grant their entire team access to a repository they personally have admin rights on, completely bypassing the organization's
RepoAdminChangeTeamAccesssetting - a setting whose entire purpose is to prevent exactly this. There's a second, equivalent endpoint for the same action that correctly enforces this setting, which is how I noticed the gap.Details
PUT /api/v1/teams/{id}/repos/{owner}/{repo}is implemented byAddTeamRepositoryinrouters/api/v1/org/team.go. It's registered inrouters/api/v1/api.gobehindreqTeamMembership(), which - per its own doc comment "user should be a team member, or a site admin" - passes for any ordinary member of the team in question (verified viaIsTeamMember), not just a team admin or org owner. Inside the handler, the only authorization check is:This checks that the caller has admin access on the repo being attached - but says nothing about whether the org allows repo admins to attach teams in the first place. There's a dedicated setting for exactly that,
RepoAdminChangeTeamAccess(defaultfalse, defined inmodels/user/user.go), and Gitea does enforce it - just not here. The other endpoint that links a team to a repo,PUT /api/v1/repos/{owner}/{repo}/teams/{team}(changeRepoTeaminrouters/api/v1/repo/teams.go), does it correctly:So with the default setting, only the org owner can link a team to a repo through that endpoint - but through
AddTeamRepository, anyone who is both a repo admin (a very common role to hand out to trusted external contributors on a single private repo) and an ordinary member of some other team can link that entire team to the repo, silently granting every current and future member of that team access to it. I confirmed this same code path is present in the current latest release, v1.26.4.PoC
Prerequisites: an account that (a) has Admin-level access on a target private repository
org/private-repo- a level of access an org owner might hand to a trusted contributor for just that one repo - and (b) is an ordinary (non-admin) member of some team in the same org, say team ID 5. The org hasRepoAdminChangeTeamAccessleft at its default (false).Impact
This is a broken access control / privilege escalation issue. It lets a non-owner who holds admin rights on a single repository silently expand access to that repository to an entire team's worth of other users, bypassing an org-level control specifically designed to prevent that. On orgs that grant per-repo admin access to external contributors (a common pattern), this could expose private repository code and history to users the org owner never intended to grant access to.
Fix
AddTeamRepository(and itsRemoveTeamRepositorycounterpart, which likely has the same gap) should enforce the same check aschangeRepoTeam: requirectx.Org.Organization.RepoAdminChangeTeamAccessor org-owner status before proceeding, instead of only checking the caller's access level on the target repo.If possible, please apply for a CVE number when publishing. I would greatly appreciate it.