fix: prevent ORM field injection via segment parameter in analytics (GHSA-93x3-ghh7-72j3) (#8864)

* 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".

* 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.
This commit is contained in:
sriram veeraghanta
2026-04-07 16:04:48 +05:30
committed by GitHub
parent 7c2fc2dd7f
commit 8a2579ce9b
2 changed files with 40 additions and 41 deletions
+14 -41
View File
@@ -29,7 +29,7 @@ from plane.db.models import (
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 @@ class AnalyticsEndpoint(BaseAPIView):
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,
@@ -214,13 +197,20 @@ class SavedAnalyticEndpoint(BaseAPIView):
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,
)
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 @@ class ExportAnalyticsEndpoint(BaseAPIView):
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,
+26
View File
@@ -22,6 +22,23 @@ from django.utils import timezone
# 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