diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 3bac1eac919..35ac7762be9 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -504,6 +504,21 @@ func reqOrgOwnership() func(ctx *context.APIContext) { } } +// reqOrgVisible requires the organization to be visible to the doer, or a site admin +func reqOrgVisible() func(ctx *context.APIContext) { + return func(ctx *context.APIContext) { + if ctx.Org.Organization == nil { + setting.PanicInDevOrTesting("reqOrgVisible: unprepared context") + ctx.APIErrorInternal(errors.New("reqOrgVisible: unprepared context")) + return + } + if !organization.HasOrgOrUserVisible(ctx, ctx.Org.Organization.AsUser(), ctx.Doer) { + ctx.APIErrorNotFound() + return + } + } +} + // reqTeamMembership user should be an team member, or a site admin func reqTeamMembership() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { @@ -1673,7 +1688,7 @@ func Routes() *web.Router { m.Combo("/{id}").Get(reqToken(), org.GetLabel). Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel). Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel) - }) + }, reqOrgVisible()) m.Group("/hooks", func() { m.Combo("").Get(org.ListHooks). Post(bind(api.CreateHookOption{}), org.CreateHook) diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go index 3306c6539cb..21123ebeb33 100644 --- a/tests/integration/api_org_test.go +++ b/tests/integration/api_org_test.go @@ -11,6 +11,7 @@ import ( "time" auth_model "gitea.dev/models/auth" + issues_model "gitea.dev/models/issues" org_model "gitea.dev/models/organization" "gitea.dev/models/perm" repo_model "gitea.dev/models/repo" @@ -292,3 +293,50 @@ func testAPIDeleteOrgRepos(t *testing.T) { MakeRequest(t, req, http.StatusNoContent) // The org contains no repositories, so the API should return StatusNoContent }) } + +// TestAPIOrgLabelsVisibility ensures the organization label read endpoints honor +// the organization visibility: labels of a private org must not be disclosed to +// users who cannot see the org (GHSA: unauthorized access to private org labels). +func TestAPIOrgLabelsVisibility(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + // privated_org (id 23) is a private organization; user5 is its only member. + privateOrg := unittest.AssertExistsAndLoadBean(t, &org_model.Organization{ID: 23}) + label := &issues_model.Label{OrgID: privateOrg.ID, Name: "internal-label", Color: "#aabbcc", Description: "private organization label"} + require.NoError(t, issues_model.NewLabel(t.Context(), label)) + + listURL := fmt.Sprintf("/api/v1/orgs/%s/labels", privateOrg.Name) + getURL := fmt.Sprintf("/api/v1/orgs/%s/labels/%d", privateOrg.Name, label.ID) + + t.Run("NonMemberDenied", func(t *testing.T) { + // user2 is not a member of the private org and must not see its labels. + token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization) + MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(token), http.StatusNotFound) + MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(token), http.StatusNotFound) + }) + + t.Run("AnonymousDenied", func(t *testing.T) { + MakeRequest(t, NewRequest(t, "GET", listURL), http.StatusNotFound) + MakeRequest(t, NewRequest(t, "GET", getURL), http.StatusNotFound) + }) + + t.Run("MemberAllowed", func(t *testing.T) { + token := getUserToken(t, "user5", auth_model.AccessTokenScopeReadOrganization) + resp := MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(token), http.StatusOK) + labels := DecodeJSON(t, resp, &[]*api.Label{}) + assert.Len(t, *labels, 1) + MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(token), http.StatusOK) + }) + + t.Run("SiteAdminAllowed", func(t *testing.T) { + token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadOrganization) + MakeRequest(t, NewRequest(t, "GET", listURL).AddTokenAuth(token), http.StatusOK) + MakeRequest(t, NewRequest(t, "GET", getURL).AddTokenAuth(token), http.StatusOK) + }) + + t.Run("PublicOrgStillReadable", func(t *testing.T) { + // org3 (id 3) is a public org with labels; non-members may read them. + token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization) + MakeRequest(t, NewRequest(t, "GET", "/api/v1/orgs/org3/labels").AddTokenAuth(token), http.StatusOK) + }) +}