An Chu Van

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()

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)

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:

CRM -> Address Template -> New Address Template

Set template to:

 //iamanc

Save the document.

image

At this stage, the payload is stored but not yet executed.

image


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

image

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. image