SSTI on ERPNEXT ≤ 15.89.0 (CVE-2025-66437)
Exploit Author: An Chu ( aka iamanc )
Vendor: Frappe Technologies Pvt. Ltd.
Product: ERPNext
Affected Versions: ERPNext ≤ 15.89.0
CVE: CVE‑2025‑66437
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:
An authenticated SSTI (Server-Side Template Injection) vulnerability exists in the get_address_display method of ERPNext. This function renders address templates using frappe.render_template() with a context derived from the address_dict parameter, which can be either a dictionary or a string referencing an Address document.
Although ERPNext uses a custom Jinja2 SandboxedEnvironment, dangerous functions like frappe.db.sql remain accessible via get_safe_globals().
An attacker with permission to create or modify an Address Template can inject arbitrary Jinja expressions into the template field. By creating an Address document with a matching country, and then calling the get_address_display API with address_dict="address_name", the system will render the malicious template using attacker-controlled data. This leads to server-side code execution or 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 frappe/contacts/doctype/address/address.py
@frappe.whitelist()
def get_address_display(address_dict: dict | str | None) -> str | None:
return render_address(address_dict)
def render_address(address: dict | str | None, check_permissions=True) -> str | None:
if not address:
return
if not isinstance(address, dict):
address = frappe.get_cached_doc("Address", address)
if check_permissions:
address.check_permission()
address = address.as_dict()
name, template = get_address_templates(address)
try:
return frappe.render_template(template, address)
except TemplateSyntaxError:
frappe.throw(_("There is an error in your Address Template {0}").format(name))
Root Cause
frappe.render_template(template, address)
template is loaded directly from the Terms and Conditions doctype.frappe.render_template() without sanitization or sandbox hardening.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:
Navigate to:
CRM -> Address Template -> New Address Template
Set template to:
//iamanc
Save the document.
At this stage, the payload is stored but not yet executed.
Step 2: Create an Address Matching the Template
Navigate to:
CRM → Address -> New Address
Create new address with Country: Algeria and save Address name like SSTI-Bug-4
Step #: Direct API Invocation
The same vulnerability can be triggered by calling the whitelisted method directly:
POST /api/method/frappe.contacts.doctype.address.address.get_address_display
When the request is processed, the injected templatepayload is rendered and executed, and the evaluated output is returned in the response.