An Chu Van

SSTI on ERPNEXT ≤ 15.89.0 (CVE-2025-66438)

Exploit Author: An Chu ( aka iamanc )

Vendor: Frappe Technologies Pvt. Ltd.

Product: ERPNext

Affected Versions: ERPNext ≤ 15.89.0

CVE: CVE‑2025‑66438

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 Server-Side Template Injection (SSTI) vulnerability exists in ERPNext’s Print Format rendering mechanism. Specifically, the API frappe.www.printview.get_html_and_style() triggers the rendering of the html field inside a Print Format document using frappe.render_template(template, doc) via the get_rendered_template() call chain.

Although ERPNext wraps Jinja2 in a SandboxedEnvironment, it exposes sensitive functions such as frappe.db.sql through get_safe_globals().

An attacker with permission to create or modify a Print Format can inject arbitrary Jinja expressions into the html field. Once the malicious Print Format is saved, the attacker can call get_html_and_style() with a target document (e.g., Supplier, Sales Invoice) to trigger the render process. This leads to information disclosure from the database, such as database version, schema details, or sensitive values depending on the injected payload.

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/frappe/www/printview.py

@frappe.whitelist()
def get_html_and_style(
	doc: str,
	name: str | None = None,
	print_format: str | None = None,
-----TRUNCATED---------
):
	"""Returns `html` and `style` of print format, used in PDF etc"""

	-----TRUNCATED---------

	try:
		html = get_rendered_template(
			doc=document,
			print_format=print_format,
			meta=document.meta,
-----TRUNCATED---------

	return {"html": html, "style": get_print_style(style=style, print_format=print_format)}
def get_rendered_template(
	doc: "Document",
	print_format: str | None = None,
	meta=None,
	no_letterhead: bool | None = None,
	letterhead: str | None = None,
	trigger_print: bool = False,
	settings: dict | None = None,
) -> str:
-----TRUNCATED---------
	# determine template
	if print_format:
		doc.print_section_headings = print_format.show_section_headings
		doc.print_line_breaks = print_format.line_breaks
		doc.align_labels_right = print_format.align_labels_right
		doc.absolute_value = print_format.absolute_value

		def get_template_from_string():
			return jenv.from_string(get_print_format(doc.doctype, print_format))

		-----TRUNCATED---------
	hook_func = frappe.get_hooks("pdf_body_html")
	html = frappe.get_attr(hook_func[-1])(jenv=jenv, template=template, print_format=print_format, args=args)

-----TRUNCATED---------
	return html
def get_print_format(doctype, print_format):
	
-----TRUNCATED---------

-----TRUNCATED---------
	if print_format.html:
		return print_format.html
	

Root Cause

The vulnerability originates from unsafe rendering of user‑controlled Print Format templates.

As a result, an authenticated attacker can inject arbitrary Jinja2 expressions, leading to Server‑Side Template Injection (SSTI).

PoC:

Step 1: Inject SSTI Payload into Print Format

Navigate to:

Print Format -> new Print Format

Set html to:

 //iamanc

Save the document.

image

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

image


Step 2: Trigger SSTI via get_html_and_style API

Navigate to:

Supplier  SUP-0001

After ERPNext frontend will call API:

POST /api/method/frappe.www.printview.get_html_and_style

image

We need change value print_format to SSTI-Bug-5

image