Repository: getsentry/sentry

Issue:
Tokens are able to enumerate project names regardless of organization membership.
We respond with 404s when there are missing project names, but 403s when there is an existing project. Probably should just check for organization membership before even going into the project fetching.
# Original HackerOne Report

Title:  Project Enumeration of Other Teams
Types:  Information Disclosure
Link:   https://hackerone.com/reports/154048
Date:   2016-07-26 05:25:05 -0700
By:     vagg-a-bond

Details:
Hello,

It is possible for any valid user of @sentry to enumerate project names in other team by comparing errors. 

For example, if user user accesses `GET /api/0/projects/sumne/project_sumne_001/` which is not of his/her, then error will be `403 FORBIDDEN` and for  `GET /api/0/projects/sumne/project_sumne_001_not_there/` application responds with `404 NOT FOUND`

So an attacker can enumerate all the projects available in a team by automating the process to request `GET /api/0/projects/TARGET_TEAM_NAME/PROJECT_NAME/`. 

**Vulnerable Endpoint:**

> GET /api/0/projects/sumne/project_sumne_001/*

**Recommended Fix:**
Respond with `404 NOT FOUND` for both.  



Discussion:

Resolving patch:
diff --git a/src/sentry/api/bases/project.py b/src/sentry/api/bases/project.py
--- a/src/sentry/api/bases/project.py
+++ b/src/sentry/api/bases/project.py
@@ -4,7 +4,7 @@
 from sentry.api.base import Endpoint
 from sentry.api.exceptions import ResourceDoesNotExist
 from sentry.api.permissions import ScopedPermission
-from sentry.models import Project, ProjectStatus
+from sentry.models import Organization, Project, ProjectStatus
 from sentry.models.apikey import ROOT_KEY
 
 
@@ -59,9 +59,21 @@ class ProjectEndpoint(Endpoint):
     permission_classes = (ProjectPermission,)
 
     def convert_args(self, request, organization_slug, project_slug, *args, **kwargs):
+        try:
+            org = Organization.objects.get_from_cache(slug=organization_slug)
+            if request.user:
+                can_access_org = any(access.from_request(request, org).memberships)
+            if request.auth:
+                can_access_org = request.auth.organization_id == org.id
+            if not can_access_org:
+                raise ResourceDoesNotExist
+
+        except Organization.DoesNotExist:
+            raise ResourceDoesNotExist
+
         try:
             project = Project.objects.get_from_cache(
-                organization__slug=organization_slug,
+                organization=org,
                 slug=project_slug,
             )
         except Project.DoesNotExist:


Test patch:
diff --git a/tests/sentry/api/endpoints/test_project_details.py b/tests/sentry/api/endpoints/test_project_details.py
--- a/tests/sentry/api/endpoints/test_project_details.py
+++ b/tests/sentry/api/endpoints/test_project_details.py
@@ -20,6 +20,18 @@ def test_simple(self):
         assert response.status_code == 200
         assert response.data['id'] == str(project.id)
 
+    def test_other_projects(self):
+        # Should return a 404 and not a 403. See https://github.com/getsentry/sentry/issues/3807
+        other_org = self.create_organization()
+        other_project = self.create_project(organization=other_org)
+        self.login_as(user=self.user)
+        url = reverse('sentry-api-0-project-details', kwargs={
+            'organization_slug': other_org.slug,
+            'project_slug': other_project.slug,
+        })
+        response = self.client.get(url)
+        assert response.status_code == 404
+
     def test_numeric_org_slug(self):
         # Regression test for https://github.com/getsentry/sentry/issues/2236
         self.login_as(user=self.user)