
ERPNext 16.16.0 - Stored XSS in POS customer section via unescaped template literals
5,1
Medium
Detected by

Fluid Attacks AI SAST Scanner
Disclosed by
Oscar Naveda
Summary
Full name
ERPNext 16.16.0 - Stored XSS in POS customer section via unescaped template literals
Code name
State
Public
Release date
Affected product
ERPNext
Vendor
Frappe
Affected version(s)
16.16.0
Vulnerability name
Stored cross-site scripting (XSS)
Vulnerability type
Remotely exploitable
Yes
CVSS v4.0 vector string
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:L/VI:L/VA:N/SC:L/SI:L/SA:N
CVSS v4.0 base score
5.1
Exploit available
No
CVE ID(s)
Description
An authenticated user can persist arbitrary HTML/JavaScript in the email_id or mobile_no fields of a Customer record and trigger unescaped rendering in the Point of Sale (POS) interface for every operator who selects that customer.
The sink is update_customer_section in erpnext/selling/page/point_of_sale/pos_item_cart.js:462, which constructs an HTML string using template literals that directly embed customer_name, email_id, mobile_no, and the output of get_customer_image() (which embeds image into src and alt attributes), then passes the result to jQuery's .html() without any HTML sanitization. The server-side whitelisted method point_of_sale.set_customer_info writes attacker-supplied values to the database with no sanitization, completing the source→sink path.
Vulnerability
Source → sink path
Source — user-controlled Customer fields via whitelisted setter (
point_of_sale.py:428-465):The method
set_customer_infois decorated with@frappe.whitelist(), which in Frappe makes it callable by any authenticated user without additional role checks:No input sanitization is performed before the database write. An attacker can set
email_idormobile_noto any HTML/JS payload by calling this endpoint directly or via the POS customer info panel.Additionally,
customer_nameandimageare writable by any user with Customer record edit permissions and are also fetched without sanitization.Insufficient sanitization —
escape()is URL-encoding, not HTML-escaping (pos_item_cart.js:470):escape()here iswindow.escape(), the deprecated global URI-encoding function (converts spaces to%20, etc.). It does not escape HTML-special characters (<,>,",&). It is applied only to thedata-customerattribute, not to any of the displayed fields (customer_name,email_id,mobile_no,image).Sink — jQuery
.html()with unescaped template literals (pos_item_cart.js:457-494):jQuery's
.html()is aninnerHTMLsetter. Any HTML tags in the interpolated values are parsed and executed by the browser. No call tofrappe.utils.escape_html,DOMPurify.sanitize, or any equivalent appears in this function.Malformed HTML in
get_customer_image()aids attribute injection (pos_item_cart.js:496-503):The template contains an extra double-quote after
alt="${image}". The closing sequence isalt="${image}""instead ofalt="${image}". Withimage = '" onerror="alert(1)':Produces:
src="" onerror="alert(1)"" alt=...The
onerrorattribute escapes the expected attribute context and executes as an event handler.
Full path from fetch to render (
pos_item_cart.js:352-395):frappe.db.get_valuereturns raw database values. After fetch,update_customer_sectionis called, placing those values directly into the DOM.
Relevant code:
erpnext/selling/page/point_of_sale/pos_item_cart.js:457-494(update_customer_section)erpnext/selling/page/point_of_sale/pos_item_cart.js:496-503(get_customer_image)erpnext/selling/page/point_of_sale/pos_item_cart.js:352-395(fetch_customer_details)erpnext/selling/page/point_of_sale/pos_item_cart.js:462(jQuery.html()sink)erpnext/selling/page/point_of_sale/pos_item_cart.js:470(escape()misuse)erpnext/selling/page/point_of_sale/point_of_sale.py:428-465(set_customer_info,@frappe.whitelist())
PoC
Reproduction used in validation environment
Environment: ERPNext 16.16.0 running locally.
Attack path 1 — via whitelisted API endpoint (any authenticated user)
Any logged-in user can call the set_customer_info endpoint directly, regardless of Customer edit permissions:
Log in to ERPNext as any authenticated user (e.g., a low-privilege sales user).
Identify a target Customer name (e.g.,
CUST-00001).Send the following request (using
curl, browser DevTools, or any HTTP client):
Confirm the payload was stored verbatim in the database:
Open the POS interface as a POS operator (a different user/session):
Navigate to
Point of Sale→ open any POS session → search for and selectCUST-00001.fetch_customer_detailsfires, loads the maliciousemail_idintothis.customer_info, andupdate_customer_sectionrenders it via.html(). Theonerrorevent fires and executesalert(document.cookie)in the operator's browser.
Attack path 2 — via Customer record (user with Customer edit permissions)
Go to
Selling → Customers → CUST-00001 → Edit.Set
Customer Nameto:Save. When any POS user selects that customer, the
<script>tag is injected via${customer_name}in the template literal and executed by jQuery.html().
Evidence of Exploitation
Video of exploitation:
Static Evidence:

Our security policy
We have reserved the ID CVE-2026-42840 to refer to this issue from now on.
System Information
ERPNext
Version 16.16.0 (branch: develop)
Operating System: Any
References
Github Repository: https://github.com/frappe/erpnext
Mitigation
There is currently no patch available for this vulnerability.
Credits
The vulnerability was discovered by Oscar Naveda from Fluid Attacks' Offensive Team using the AI SAST Scanner.
Timeline
Vulnerability discovered
Vendor contacted
Public disclosure
Does your application use this vulnerable software?
During our free trial, our tools assess your application, identify vulnerabilities, and provide recommendations for their remediation.













