Skip to content

Commit 8a2579c

Browse files
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.
1 parent 7c2fc2d commit 8a2579c

2 files changed

Lines changed: 40 additions & 41 deletions

File tree

apps/api/plane/app/views/analytic/base.py

Lines changed: 14 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
Module,
3030
)
3131

32-
from plane.utils.analytics_plot import build_graph_plot
32+
from plane.utils.analytics_plot import build_graph_plot, VALID_ANALYTICS_FIELDS, VALID_YAXIS
3333
from plane.utils.issue_filters import issue_filters
3434
from plane.app.permissions import allow_permission, ROLE
3535

@@ -41,32 +41,15 @@ def get(self, request, slug):
4141
y_axis = request.GET.get("y_axis", False)
4242
segment = request.GET.get("segment", False)
4343

44-
valid_xaxis_segment = [
45-
"state_id",
46-
"state__group",
47-
"labels__id",
48-
"assignees__id",
49-
"estimate_point__value",
50-
"issue_cycle__cycle_id",
51-
"issue_module__module_id",
52-
"priority",
53-
"start_date",
54-
"target_date",
55-
"created_at",
56-
"completed_at",
57-
]
58-
59-
valid_yaxis = ["issue_count", "estimate"]
60-
6144
# Check for x-axis and y-axis as thery are required parameters
62-
if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis:
45+
if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS:
6346
return Response(
6447
{"error": "x-axis and y-axis dimensions are required and the values should be valid"},
6548
status=status.HTTP_400_BAD_REQUEST,
6649
)
6750

6851
# If segment is present it cannot be same as x-axis
69-
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
52+
if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment):
7053
return Response(
7154
{"error": "Both segment and x axis cannot be same and segment should be valid"},
7255
status=status.HTTP_400_BAD_REQUEST,
@@ -214,13 +197,20 @@ def get(self, request, slug, analytic_id):
214197
x_axis = analytic_view.query_dict.get("x_axis", False)
215198
y_axis = analytic_view.query_dict.get("y_axis", False)
216199

217-
if not x_axis or not y_axis:
200+
if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS:
218201
return Response(
219-
{"error": "x-axis and y-axis dimensions are required"},
202+
{"error": "x-axis and y-axis dimensions are required and the values should be valid"},
220203
status=status.HTTP_400_BAD_REQUEST,
221204
)
222205

223206
segment = request.GET.get("segment", False)
207+
208+
if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment):
209+
return Response(
210+
{"error": "Both segment and x axis cannot be same and segment should be valid"},
211+
status=status.HTTP_400_BAD_REQUEST,
212+
)
213+
224214
distribution = build_graph_plot(queryset=queryset, x_axis=x_axis, y_axis=y_axis, segment=segment)
225215
total_issues = queryset.count()
226216
return Response(
@@ -236,32 +226,15 @@ def post(self, request, slug):
236226
y_axis = request.data.get("y_axis", False)
237227
segment = request.data.get("segment", False)
238228

239-
valid_xaxis_segment = [
240-
"state_id",
241-
"state__group",
242-
"labels__id",
243-
"assignees__id",
244-
"estimate_point",
245-
"issue_cycle__cycle_id",
246-
"issue_module__module_id",
247-
"priority",
248-
"start_date",
249-
"target_date",
250-
"created_at",
251-
"completed_at",
252-
]
253-
254-
valid_yaxis = ["issue_count", "estimate"]
255-
256229
# Check for x-axis and y-axis as thery are required parameters
257-
if not x_axis or not y_axis or x_axis not in valid_xaxis_segment or y_axis not in valid_yaxis:
230+
if not x_axis or not y_axis or x_axis not in VALID_ANALYTICS_FIELDS or y_axis not in VALID_YAXIS:
258231
return Response(
259232
{"error": "x-axis and y-axis dimensions are required and the values should be valid"},
260233
status=status.HTTP_400_BAD_REQUEST,
261234
)
262235

263236
# If segment is present it cannot be same as x-axis
264-
if segment and (segment not in valid_xaxis_segment or x_axis == segment):
237+
if segment and (segment not in VALID_ANALYTICS_FIELDS or x_axis == segment):
265238
return Response(
266239
{"error": "Both segment and x axis cannot be same and segment should be valid"},
267240
status=status.HTTP_400_BAD_REQUEST,

apps/api/plane/utils/analytics_plot.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@
2222
# Module imports
2323
from plane.db.models import Issue, Project
2424

25+
VALID_ANALYTICS_FIELDS = [
26+
"state_id",
27+
"state__group",
28+
"labels__id",
29+
"assignees__id",
30+
"estimate_point__value",
31+
"issue_cycle__cycle_id",
32+
"issue_module__module_id",
33+
"priority",
34+
"start_date",
35+
"target_date",
36+
"created_at",
37+
"completed_at",
38+
]
39+
40+
VALID_YAXIS = ["issue_count", "estimate"]
41+
2542

2643
def annotate_with_monthly_dimension(queryset, field_name, attribute):
2744
# Get the year and the months
@@ -34,6 +51,8 @@ def annotate_with_monthly_dimension(queryset, field_name, attribute):
3451

3552

3653
def extract_axis(queryset, x_axis):
54+
if x_axis not in VALID_ANALYTICS_FIELDS:
55+
raise ValueError(f"Invalid x_axis value: {x_axis}")
3756
# Format the dimension when the axis is in date
3857
if x_axis in ["created_at", "start_date", "target_date", "completed_at"]:
3958
queryset = annotate_with_monthly_dimension(queryset, x_axis, "dimension")
@@ -52,6 +71,13 @@ def sort_data(data, temp_axis):
5271

5372

5473
def build_graph_plot(queryset, x_axis, y_axis, segment=None):
74+
if x_axis not in VALID_ANALYTICS_FIELDS:
75+
raise ValueError(f"Invalid x_axis value: {x_axis}")
76+
if y_axis not in VALID_YAXIS:
77+
raise ValueError(f"Invalid y_axis value: {y_axis}")
78+
if segment and segment not in VALID_ANALYTICS_FIELDS:
79+
raise ValueError(f"Invalid segment value: {segment}")
80+
5581
# temp x_axis
5682
temp_axis = x_axis
5783
# Extract the x_axis and queryset

0 commit comments

Comments
 (0)