Authorization analysis logic improvements (#61)

This commit is contained in:
Eli Peter 2026-05-02 16:44:49 -04:00 committed by GitHub
parent 3c89bddbf2
commit 40995e45e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 4193 additions and 134 deletions

View file

@ -0,0 +1,20 @@
# py-auth-vuln-002: helper takes a user-supplied id (`project_id`)
# and queries by it without any preceding ownership/membership check.
# This is the vulnerable counterpart to
# safe_django_orm_caller_scoped_entity.py — same Django ORM shape, but
# the param is an *id-like user input*, not a scope-entity object, so
# the caller-scope-entity exemption must not apply.
#
# Pinned to keep recall on the missing_ownership_check rule.
class Project:
pass
def get_project(request, project_id):
return Project.objects.filter(id=project_id).first()
def delete_project(request, project_id):
Project.objects.filter(id=project_id).delete()

View file

@ -0,0 +1,63 @@
# py-auth-realrepo-008: caller-passed scope entity used as ownership
# constraint. Distilled from sentry
# `src/sentry/api/helpers/environments.py::get_environments` (and the
# many sibling helpers in `api/endpoints/organization_releases.py`):
#
# def get_environments(request, organization: Organization):
# ...
# return list(
# Environment.objects.filter(
# organization_id=organization.id,
# name__in=requested_environments,
# )
# )
#
# `_filter_releases_by_query(queryset, organization, query, filter_params)`
# follows the same pattern with `queryset.filter_by_semver(organization.id, ...)`.
#
# Both helpers receive the already-authorised `organization` object
# from a route handler that resolved it via `OrganizationReleasesBaseEndpoint`
# membership middleware. The query is *scoped by* `organization.id`
# — that IS the ownership boundary, not a user-controlled target.
#
# Without the caller-scope-entity exemption, every internal helper in a
# multi-tenant Django/Rails/Laravel codebase flags
# `missing_ownership_check` because the engine cannot tell "scoping
# arg" from "user-targeted arg". The fix recognises that
# `<entity>.id` where `<entity>` is a unit parameter named after a
# scope-bearing domain entity (organization, project, team, workspace,
# tenant, account, ...) is a passed-in scope, not a target.
from typing import List
class Organization:
pass
class Environment:
pass
def get_environments(request, organization: Organization) -> List[Environment]:
requested_environments = set(request.GET.getlist("environment"))
if not requested_environments:
return []
return list(
Environment.objects.filter(
organization_id=organization.id, name__in=requested_environments
)
)
def _filter_releases_by_query(queryset, organization: Organization, query, filter_params):
queryset = queryset.filter_by_semver(organization.id, query)
queryset = queryset.filter_by_stage(organization.id, query)
return queryset
def list_project_issues(request, project):
return list(Issue.objects.filter(project_id=project.id, status="open"))
class Issue:
pass

View file

@ -0,0 +1,52 @@
# py-auth-realrepo-010: pytest test method decorated with
# `@mock.patch("...")` collides with Flask's `<app>.<verb>` route
# decorator shape (bare_method_name("mock.patch") == "patch", which the
# parse_flask_route_decorator matched as HTTP PATCH). The collision
# attached the test method as a Flask route handler, flipped its
# `unit.kind` to RouteHandler, made it pass
# `unit_has_user_input_evidence` unconditionally, and flooded pytest
# test suites with `missing_ownership_check` findings.
#
# Distilled from airflow
# `providers/google/tests/unit/google/cloud/hooks/test_dlp.py` (47 FPs
# in this single file pre-fix). Fix:
# `parse_flask_route_decorator` short-circuits when the callee text
# matches a known test-framework decorator vocabulary
# (`mock.patch`, `unittest.mock.patch`, `monkeypatch.setattr`,
# `pytest.mark.parametrize`, …).
#
# This fixture verifies pytest test methods don't fire ownership-check
# findings, even when they call ORM-shaped APIs with id-suffixed
# constants (the canonical pytest fixture-data pattern).
from unittest import mock
from unittest.mock import PropertyMock
ORGANIZATION_ID = "fake-org-id-123"
PROJECT_ID = "fake-proj-id-456"
DLP_JOB_ID = "fake-job-id-789"
class TestCloudDLPHook:
@mock.patch(
"module.GoogleBaseHook.project_id",
new_callable=PropertyMock,
return_value=None,
)
@mock.patch("module.CloudDLPHook.get_conn")
def test_create_deidentify_template_with_org_id(self, get_conn, mock_project_id):
get_conn.return_value.create_deidentify_template.return_value = "API_RESPONSE"
result = self.hook.create_deidentify_template(organization_id=ORGANIZATION_ID)
return result
@mock.patch("module.CloudDLPHook.get_conn")
def test_create_dlp_job(self, get_conn):
result = self.hook.create_dlp_job(project_id=PROJECT_ID)
return result
@mock.patch.object(SomeClass, "method")
def test_with_object_patch(self, mock_method):
self.hook.cancel_dlp_job(dlp_job_id=DLP_JOB_ID)
class SomeClass:
pass