From d987141aee4b258f6e3003aa2a6e0a04a9da2209 Mon Sep 17 00:00:00 2001 From: sriramveeraghanta Date: Tue, 7 Apr 2026 14:21:21 +0530 Subject: [PATCH 1/2] fix: prevent ORM field injection via segment parameter in analytics (GHSA-93x3-ghh7-72j3) Centralize analytics field allowlists into VALID_ANALYTICS_FIELDS and VALID_YAXIS constants in analytics_plot.py. Add defense-in-depth validation in build_graph_plot() and extract_axis() so no caller can pass arbitrary field references to Django F() expressions. Add missing segment validation to SavedAnalyticEndpoint. Also fixes ExportAnalytics using "estimate_point" instead of "estimate_point__value". --- apps/api/plane/app/views/analytic/base.py | 51 +++++-------------- .../api/plane/bgtasks/analytic_plot_export.py | 2 +- apps/api/plane/utils/analytics_plot.py | 26 ++++++++++ 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/apps/api/plane/app/views/analytic/base.py b/apps/api/plane/app/views/analytic/base.py index 2f3f8b5737d..b120d8a97e7 100644 --- a/apps/api/plane/app/views/analytic/base.py +++ b/apps/api/plane/app/views/analytic/base.py @@ -29,7 +29,7 @@ Module, ) -from plane.utils.analytics_plot import build_graph_plot +from plane.utils.analytics_plot import build_graph_plot, VALID_ANALYTICS_FIELDS, VALID_YAXIS from plane.utils.issue_filters import issue_filters from plane.app.permissions import allow_permission, ROLE @@ -41,32 +41,15 @@ def get(self, request, slug): y_axis = request.GET.get("y_axis", False) segment = request.GET.get("segment", False) - valid_xaxis_segment = [ - "state_id", - "state__group", - "labels__id", - "assignees__id", - "estimate_point__value", - "issue_cycle__cycle_id", - "issue_module__module_id", - "priority", - "start_date", - "target_date", - "created_at", - "completed_at", - ] - - valid_yaxis = ["issue_count", "estimate"] - # Check for x-axis and y-axis as thery are required parameters - if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis: + if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS: return Response( {"error": "x-axis and y-axis dimensions are required and the values should be valid"}, status=status.HTTP_400_BAD_REQUEST, ) # If segment is present it cannot be same as x-axis - if segment and (segment not in valid_xaxis_segment or x_axis == segment): + if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment): return Response( {"error": "Both segment and x axis cannot be same and segment should be valid"}, status=status.HTTP_400_BAD_REQUEST, @@ -221,6 +204,13 @@ def get(self, request, slug, analytic_id): ) segment = request.GET.get("segment", False) + + if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment): + return Response( + {"error": "Both segment and x axis cannot be same and segment should be valid"}, + status=status.HTTP_400_BAD_REQUEST, + ) + distribution = build_graph_plot(queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment) total_issues = queryset.count() return Response( @@ -236,32 +226,15 @@ def post(self, request, slug): y_axis = request.data.get("y_axis", False) segment = request.data.get("segment", False) - valid_xaxis_segment = [ - "state_id", - "state__group", - "labels__id", - "assignees__id", - "estimate_point", - "issue_cycle__cycle_id", - "issue_module__module_id", - "priority", - "start_date", - "target_date", - "created_at", - "completed_at", - ] - - valid_yaxis = ["issue_count", "estimate"] - # Check for x-axis and y-axis as thery are required parameters - if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis: + if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS: return Response( {"error": "x-axis and y-axis dimensions are required and the values should be valid"}, status=status.HTTP_400_BAD_REQUEST, ) # If segment is present it cannot be same as x-axis - if segment and (segment not in valid_xaxis_segment or x_axis == segment): + if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment): return Response( {"error": "Both segment and x axis cannot be same and segment should be valid"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apps/api/plane/bgtasks/analytic_plot_export.py b/apps/api/plane/bgtasks/analytic_plot_export.py index 4b0983138be..5b226f0157b 100644 --- a/apps/api/plane/bgtasks/analytic_plot_export.py +++ b/apps/api/plane/bgtasks/analytic_plot_export.py @@ -20,7 +20,7 @@ # Module imports from plane.db.models import Issue from plane.license.utils.instance_value import get_email_configuration -from plane.utils.analytics_plot import build_graph_plot +from plane.utils.analytics_plot import build_graph_plot, VALID_ANALYTICS_FIELDS, VALID_YAXIS from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception from plane.utils.issue_filters import issue_filters diff --git a/apps/api/plane/utils/analytics_plot.py b/apps/api/plane/utils/analytics_plot.py index acd86aca868..5a09f12fdb6 100644 --- a/apps/api/plane/utils/analytics_plot.py +++ b/apps/api/plane/utils/analytics_plot.py @@ -22,6 +22,23 @@ # Module imports from plane.db.models import Issue, Project +VALID_ANALYTICS_FIELDS = [ + "state_id", + "state__group", + "labels__id", + "assignees__id", + "estimate_point__value", + "issue_cycle__cycle_id", + "issue_module__module_id", + "priority", + "start_date", + "target_date", + "created_at", + "completed_at", +] + +VALID_YAXIS = ["issue_count", "estimate"] + def annotate_with_monthly_dimension(queryset, field_name, attribute): # Get the year and the months @@ -34,6 +51,8 @@ def annotate_with_monthly_dimension(queryset, field_name, attribute): def extract_axis(queryset, x_axis): + if x_axis not in VALID_ANALYTICS_FIELDS: + raise ValueError(f"Invalid x_axis value: {x_axis}") # Format the dimension when the axis is in date if x_axis in ["created_at", "start_date", "target_date", "completed_at"]: queryset = annotate_with_monthly_dimension(queryset, x_axis, "dimension") @@ -52,6 +71,13 @@ def sort_data(data, temp_axis): def build_graph_plot(queryset, x_axis, y_axis, segment=None): + if x_axis not in VALID_ANALYTICS_FIELDS: + raise ValueError(f"Invalid x_axis value: {x_axis}") + if y_axis not in VALID_YAXIS: + raise ValueError(f"Invalid y_axis value: {y_axis}") + if segment and segment not in VALID_ANALYTICS_FIELDS: + raise ValueError(f"Invalid segment value: {segment}") + # temp x_axis temp_axis = x_axis # Extract the x_axis and queryset From 181983426172287a7e9fbe52eb023d7a18ecd851 Mon Sep 17 00:00:00 2001 From: sriramveeraghanta Date: Tue, 7 Apr 2026 14:33:14 +0530 Subject: [PATCH 2/2] fix: address PR review - remove unused imports and validate stored query params Remove unused VALID_ANALYTICS_FIELDS and VALID_YAXIS imports from analytic_plot_export.py. Add x_axis/y_axis allowlist validation in SavedAnalyticEndpoint for stored query_dict values to prevent 500 errors from malformed saved analytics. --- apps/api/plane/app/views/analytic/base.py | 4 ++-- apps/api/plane/bgtasks/analytic_plot_export.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/api/plane/app/views/analytic/base.py b/apps/api/plane/app/views/analytic/base.py index b120d8a97e7..a05712c4ecf 100644 --- a/apps/api/plane/app/views/analytic/base.py +++ b/apps/api/plane/app/views/analytic/base.py @@ -197,9 +197,9 @@ def get(self, request, slug, analytic_id): x_axis = analytic_view.query_dict.get("x_axis", False) y_axis = analytic_view.query_dict.get("y_axis", False) - if not x_axis or not y_axis: + if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS: return Response( - {"error": "x-axis and y-axis dimensions are required"}, + {"error": "x-axis and y-axis dimensions are required and the values should be valid"}, status=status.HTTP_400_BAD_REQUEST, ) diff --git a/apps/api/plane/bgtasks/analytic_plot_export.py b/apps/api/plane/bgtasks/analytic_plot_export.py index 5b226f0157b..4b0983138be 100644 --- a/apps/api/plane/bgtasks/analytic_plot_export.py +++ b/apps/api/plane/bgtasks/analytic_plot_export.py @@ -20,7 +20,7 @@ # Module imports from plane.db.models import Issue from plane.license.utils.instance_value import get_email_configuration -from plane.utils.analytics_plot import build_graph_plot, VALID_ANALYTICS_FIELDS, VALID_YAXIS +from plane.utils.analytics_plot import build_graph_plot from plane.utils.email import generate_plain_text_from_html from plane.utils.exception_logger import log_exception from plane.utils.issue_filters import issue_filters