mirror of
https://github.com/go-gitea/gitea.git
synced 2026-06-18 00:43:38 +02:00
fix(api): don't expose private org membership via public_members
The GET /orgs/{org}/public_members and /public_members/{username}
endpoints returned membership information without checking whether the
requester is allowed to see the organization. For a private org, any
authenticated user could probe public_members/{username} and infer
membership from the 204 vs 404 response, disclosing data that is hidden
in the web UI.
Gate both handlers on HasOrgOrUserVisible so they return 404 when the
doer cannot see the organization, matching the existing behaviour of the
org GET endpoint.
Assisted-by: Claude:claude-opus-4-8
This commit is contained in:
parent
b7bd222e87
commit
2828e4bf72
@ -119,6 +119,12 @@ func ListPublicMembers(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// don't disclose membership of organizations the doer cannot see
|
||||
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
|
||||
listMembers(ctx, false)
|
||||
}
|
||||
|
||||
@ -201,6 +207,11 @@ func IsPublicMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
// don't disclose membership of organizations the doer cannot see
|
||||
if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) {
|
||||
ctx.APIErrorNotFound()
|
||||
return
|
||||
}
|
||||
is, err := organization.IsPublicMembership(ctx, ctx.Org.Organization.ID, userToCheck.ID)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
|
||||
@ -263,6 +263,32 @@ func testAPIOrgGeneral(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIOrgPrivateMembersNotLeaked(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// privated_org (org 23) has private visibility and a single member, user5
|
||||
const orgName = "privated_org"
|
||||
const memberName = "user5"
|
||||
|
||||
// member publicizes their own membership inside the private org
|
||||
memberSession := loginUser(t, memberName)
|
||||
memberToken := getTokenForLoggedInUser(t, memberSession, auth_model.AccessTokenScopeWriteOrganization)
|
||||
req := NewRequest(t, "PUT", "/api/v1/orgs/"+orgName+"/public_members/"+memberName).AddTokenAuth(memberToken)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
|
||||
// an outsider must not be able to learn about the membership of a private org
|
||||
outsiderSession := loginUser(t, "user2")
|
||||
outsiderToken := getTokenForLoggedInUser(t, outsiderSession, auth_model.AccessTokenScopeReadOrganization)
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs/"+orgName+"/public_members/"+memberName).AddTokenAuth(outsiderToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs/"+orgName+"/public_members").AddTokenAuth(outsiderToken)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
// the member can still see the public membership of their own org
|
||||
req = NewRequest(t, "GET", "/api/v1/orgs/"+orgName+"/public_members/"+memberName).AddTokenAuth(memberToken)
|
||||
MakeRequest(t, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func testAPIDeleteOrgRepos(t *testing.T) {
|
||||
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
|
||||
orgRepos, err := repo_model.GetOrgRepositories(t.Context(), org3.ID)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user