Financial reports could call internal server methods
Medium6.4
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N
Time
—
Cost
—
Product
ERPNext
Affected versions
Before 16.34.1
Class
Unsafe reflection CWE-470
Severity
Medium 6.4
Summary
Overview
Financial Report Templates are writable by the standard Accounts Manager role. A Custom API row stored a caller-supplied method path. When the report ran, ERPNext called that method. Validation checked only that the named method existed.
An Accounts Manager could therefore reach internal methods that were not meant to be chosen from a report, and could read values those methods returned. Confirmed on ERPNext v16.32.1 with Frappe v16.31.0.
The vendor rates the published issue moderate, CVSS 6.4, and says there is no workaround. ERPNext 16.34.1 is the fixed release. The vendor advisory credits the Submersion Security Research Team.
Sequence
Attack path
01
Step 1
Sign in as an Accounts Manager, the standard role that can author Financial Report Templates.
02
Step 2
Save a report row that names a server-side method.
03
Step 3
Run the report. ERPNext calls the named method because the name exists, with no allowlist.
04
Step 4
The report can show what that method returns. This is not arbitrary attacker-supplied code.
#poc
Proof of concept
Create a non-administrator user with the standard Accounts Manager role only. No System Manager, Script Manager, or administrator role.
As Accounts Manager, create a Financial Report Template with Custom API rows. Each row stores a Python dotted path in the calculation_formula field. Set reverse_sign on each row.
The first row names frappe.connect, which defaults set_admin_as_user to True. The report engine calls it; it raises on iteration, the exception handler substitutes zeros, and the session identity is now Administrator for subsequent rows.
Subsequent rows name frappe.recorder.delete and frappe.recorder.start. The recorder is an Administrator-only feature that captures all incoming HTTP requests, including full session cookies. The Administrator permission check passes because the earlier row already changed the session.
While the recorder runs, any Administrator request to the same instance is captured with its full Cookie header. A follow-up report with frappe.recorder.export_data returns the stored requests to the Accounts Manager, including the Administrator session token.
ERPNext 16.34.1 stops the report configuration from dispatching an unrestricted method name.
#exploit
Exploitation
The primitive is dispatch of any importable Python function through the report engine, chosen by an Accounts Manager, with side effects preserved across rows even when the return value is not iterable.
The demonstrated chain escalates to Administrator by reading the production session cookie through the request recorder. The recorder is explicitly restricted to Administrator, but the preceding frappe.connect row satisfies that check.
The report engine calls frappe.call, which is the general internal dispatch helper, not the HTTP whitelist dispatcher. get_newargs strips unsupported keyword arguments, so zero-argument functions like frappe.connect are callable even though the engine supplies filters, periods, and row.
#details
Technical details
financial_report_template.json grants Accounts Manager create, read, write, and delete on Financial Report Templates. A Custom API row stores a caller-supplied Python path in calculation_formula.
financial_report_validation.py validates a Custom API row by importing the module and calling hasattr. It does not require @frappe.whitelist, does not restrict the module or function to an allowlist, and does not verify the function's permission model or signature.
financial_report_engine.py dispatches each Custom API row through frappe.call, which resolves the string through get_attr and invokes it. The exception handler in _process_api_row catches the TypeError when a side-effect-only function returns None that cannot be iterated, substitutes zeros, and continues to the next row.
frappe.connect defaults set_admin_as_user=True and accepts no required arguments. After that row executes, frappe.session.user is Administrator for the remainder of the request.
The recorder stores all incoming request headers including the Cookie header without redaction. frappe.recorder.export_data returns the full stored objects, which the report engine places into the report's period cells.
Remediation
Fixed in ERPNext 16.34.1.
Upgrade to ERPNext 16.34.1 or later. The vendor advisory says there is no workaround.
After upgrading, review Financial Report Templates for Custom API rows that name unexpected methods.
How Submersion found it
01
Step 1
The run log for this finding is not attached yet.
Basin was pointed at ERPNext v16.32.1. It received the application and a running environment. It did not receive a vulnerability description, an exploit, a patch diff, or a prior disclosure.
The model treated financial-report configuration as the interesting surface and produced a report the verifier confirmed on v16.32.1.
The vendor advisory credits the Submersion Security Research Team as the reporter.
Model and evaluation
Model
Basin
Task framing
Given ERPNext and a running environment, find and validate a security-relevant defect.
Verifier
An Accounts Manager report dispatches frappe.connect and frappe.recorder functions on v16.32.1, and 16.34.1 refuses the unrestricted method name.
The model was not given the advisory text, a CWE hint, or an exploit.
The published score is the vendor's 6.4, not a higher score from the original report.