fix(security): scope cycle/module lookup by workspace in advance analytics chart - #9685
fix(security): scope cycle/module lookup by workspace in advance analytics chart#9685jadhavgaurav wants to merge 1 commit into
Conversation
…ytics chart
ProjectAdvanceAnalyticsChartEndpoint's work-items chart looked up the Cycle and
Module named by the cycle_id/module_id query params with a bare
Cycle.objects.filter(id=cycle_id) / Module.objects.filter(id=module_id), with no
workspace filter. The CycleIssue/ModuleIssue querysets used to build the chart
were correctly scoped via base_filters (which includes workspace__slug), so they
returned no issues for a foreign ID, but the cycle/module lookup itself was not.
That let any member of workspace A pass a cycle_id or module_id belonging to
workspace B and get that cycle's/module's start_date and end_date back in the
chart response, since those fields drive the date range used to build the
per-day series. Iterating UUIDs would let an attacker enumerate cycle/module date
ranges across every workspace.
Both lookups are now scoped with workspace__slug=self._workspace_slug, matching
every other query in this file (base_filters always includes workspace__slug,
and the sibling ProjectAdvanceAnalyticsEndpoint/StatsEndpoint scope Cycle/Module
counts the same way). A foreign ID now resolves to no cycle/module, so the
existing "not found" branch returns the same empty {"data": [], "schema": {}}
it already returns for a cycle/module with no start_date.
Added contract tests asserting a foreign workspace's cycle/module dates are not
returned (this fails without the fix, leaking the foreign date range in the
generated series), plus a positive control confirming a cycle in the requester's
own workspace still returns its chart data.
Fixes makeplane#9601
Co-authored-by: Claude <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review. 📝 WalkthroughWalkthroughThe analytics endpoint now scopes cycle and module lookups to the current workspace. Contract tests verify that foreign workspace dates are excluded and local workspace data remains available. ChangesAnalytics workspace scoping
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The change scopes cycle and module lookups to the requested workspace while preserving same-workspace chart behavior; no actionable merge-blocking risk remains after normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Description
ProjectAdvanceAnalyticsChartEndpoint.work_item_completion_chart(inapps/api/plane/app/views/analytic/project_analytics.py) looked up theCycle/Modulenamed by thecycle_id/module_idquery params with a bareCycle.objects.filter(id=cycle_id)/Module.objects.filter(id=module_id)— no workspace filter.The
CycleIssue/ModuleIssuequerysets used to build the chart are correctly scoped viabase_filters(which includesworkspace__slug), so they return no issues for a foreign ID. But theCycle/Modulelookup itself was unscoped, so it succeeded for any cycle/module UUID in the database. Itsstart_date/end_datedrive the date range used to build the per-day series in the response, so a member of workspace A could pass acycle_id/module_idbelonging to workspace B and get that foreign cycle's/module's start and end dates back, byte for byte, in the chart response — an IDOR that lets an attacker enumerate cycle/module date ranges across every workspace by iterating UUIDs.Fix: scope both lookups with
workspace__slug=self._workspace_slug, matching every other query in this file (base_filtersalways includesworkspace__slug, and the siblingProjectAdvanceAnalyticsEndpoint/StatsEndpointscope theirCycle/Modulecounts the same way). A foreign ID now resolves to no cycle/module, so the existing "not found" branch returns the same{"data": [], "schema": {}}it already returns when a cycle/module has nostart_date.This is a minimal, two-line change (plus a regression test) — no new authorization mechanism, just closing the missing filter to match the existing pattern in the same file.
Type of Change
Screenshots and Media (if applicable)
N/A — backend-only authorization fix.
Test Scenarios
Added
apps/api/plane/tests/contract/app/test_project_advance_analytics_workspace_scope_app.py:cycle_idfrom workspace B gets{"data": [], "schema": {}}— no leaked dates (fails without the fix, previously leaked the foreign cycle's real date range).module_id.cycle_idbelonging to the requester's own workspace still returns populated chart data.Ran locally against a local Postgres instance:
Also ran the broader cycle/module/analytics contract suite (
-k "cycle or module or analytic") — 13 passed, 4 pre-existing failures unrelated to this change (they fail identically onpreviewwith or without this fix, due to a missing RabbitMQ broker /APP_BASE_URLin this local environment, not to any behavior this PR touches).ruff checkandruff format --checkpass on both changed files.References
Fixes #9601
Disclosure: this change was prepared with AI assistance (Claude), then reviewed, tested, and submitted by a human.
Summary by CodeRabbit