SECURITY ADVISORY / 01

CVE-2026-66000 Exploit & Vulnerability Analysis

Complete CVE-2026-66000 security advisory with proof of concept (PoC), exploit details, and patch analysis for frappe.

frappe products NVD ↗
Exploit PoC Vulnerability Patch Analysis

The Exploit

An attacker whose document read permissions have been revoked or restricted can continue to receive email notifications containing the full document data and field-level changes by remaining in the Document Follow subscription list.

POST /api/resource/Document%20Follow HTTP/1.1
Host: target-frappe-instance.com
Content-Type: application/json
Authorization: Bearer <valid-but-restricted-token>

{
  "ref_doctype": "Sales Order",
  "ref_docname": "SO-2024-001",
  "user": "[email protected]"
}

When the Document Follow notification is generated and sent via email, the attacker receives the full changed values, field names, and child table modifications—even though their role no longer grants read access to the Sales Order doctype. The email lands in their inbox containing sensitive fields like pricing, customer PII, and internal notes that should be inaccessible under the new permission model.

What the Patch Did

Before:

def get_message_for_user(user, frequency):
    messages = {}
    latest_document_follows = frappe.get_list(
        "Document Follow",
        filters={"user": user, "enabled": 1},
        fields=["ref_doctype", "ref_docname", "creation"]
    )
    for document_follow in latest_document_follows:
        content = get_message(
            document_follow.ref_docname, 
            document_follow.ref_doctype, 
            frequency, 
            user
        )
        messages[f"{document_follow.ref_doctype}:{document_follow.ref_docname}"] = content

def get_field_changed(changed, time, doctype, doc_name, v):
    items = []
    for d in changed:
        items.append(f"{d[0]}: {d[1]}")
    return items

After:

def get_message_for_user(user, frequency):
    messages = {}
    latest_document_follows = frappe.get_list(
        "Document Follow",
        filters={"user": user, "enabled": 1},
        fields=["ref_doctype", "ref_docname", "creation"]
    )
    for document_follow in latest_document_follows:
        if not frappe.has_permission(
            document_follow.ref_doctype, 
            "read", 
            doc=document_follow.ref_docname, 
            user=user
        ):
            frappe.db.delete(
                "Document Follow",
                {
                    "ref_doctype": document_follow.ref_doctype,
                    "ref_docname": document_follow.ref_docname,
                    "user": user,
                }
            )
            continue
        content = get_message(...)
        messages[...] = content

def get_field_changed(changed, time, doctype, doc_name, v, user):
    items = []
    permitted_fieldnames = frappe.get_meta(doctype).get_permitted_fieldnames(
        permission_type="read", user=user
    )
    for d in changed:
        if d[0] not in permitted_fieldnames:
            continue
        items.append(f"{d[0]}: {d[1]}")
    return items

The patch introduced three overlapping security controls: (1) a document-level frappe.has_permission() check in the notification generation loop that deletes stale follows from revoked users; (2) a field-level permission filter using get_permitted_fieldnames() that restricts which field changes are included in the email body; and (3) the same control applied to child table change tracking. Each control operates at a different layer—document access, field visibility, and row visibility respectively.

Root Cause

CWE-862: Missing Authorization. The Document Follow notification system cached follow relationships in the database without re-evaluating the follower's current permissions at notification send time. When a user's role was updated, revoked, or scoped, the system never checked whether that user still retained read access to the followed document before including its data in the email payload. The dataflow is straightforward: a follower's database row (ref_doctype, ref_docname, user) flows directly into get_message() without passing through frappe.has_permission(), crossing the trust boundary from stored state into an output channel (email) without validation.

Why It Works

The load-bearing line is the frappe.has_permission(document_follow.ref_doctype, "read", ...) check in the notification loop. Without it, the follow record still exists in the database and the notification generator still calls get_message() and compiles the email. Removing that single line resurrects the vulnerability entirely. The subsequent field-level filter in get_field_changed() and get_row_changed() is defense-in-depth: it ensures that even if somehow a revoked user's follow survives to the email generation stage, sensitive fields are redacted. The engineer added the field-level checks because permission models are often hierarchical—a user might lose access to a document entirely, or might retain read access to the document but only certain fields (e.g., a sales rep can see customer name but not cost). Filtering at both layers makes the control resilient to future permission model changes.

Hardening Checklist

  • Add permission re-evaluation before notification dispatch: For any subscription, saved-search, or follow feature that generates emails or reports, call the authorization framework (e.g., frappe.has_permission()) on every included document at send time, not just at subscription creation time. Store no implicit assumption that past permissions remain current.

  • Filter sensitive fields in output templates: Use the framework's field-level permission API (e.g., get_permitted_fieldnames() in Frappe, or equivalent field-level ACL in your framework) to whitelist which fields appear in emails. Never output all changed fields unconditionally.

  • Audit follow/subscription deletion on role changes: Implement a hook that fires when a user's role or permissions are updated, and delete or flag Document Follow records for documents the user can no longer read. This prevents stale follows from existing in the first place.

  • Log notification generation with audit context: Record which user triggered the notification and what fields were included in the email body. This makes post-breach forensics viable and surfaces over-permissive notifications during code review.

  • Test permission revocation end-to-end: In the test suite, create a follow as User A, revoke User A's access, regenerate notifications, and assert the email is not sent or contains no data. This is not tested by default because it crosses the boundary between permission state and notification state.

References

  • https://nvd.nist.gov/vuln/detail/CVE-2026-66000

Frequently asked questions about CVE-2026-66000

What is CVE-2026-66000?

CVE-2026-66000 is a security vulnerability identified in frappe. This security advisory provides detailed technical analysis of the vulnerability, exploit methodology, affected versions, and complete remediation guidance.

Is there a PoC (proof of concept) for CVE-2026-66000?

Yes. This writeup includes proof-of-concept details and a technical exploit breakdown for CVE-2026-66000. Review the analysis sections above for the PoC walkthrough and code examples.

How does CVE-2026-66000 get exploited?

The technical analysis section explains the vulnerability mechanics, attack vectors, and exploitation methodology affecting frappe. PatchLeaks publishes this information for defensive and educational purposes.

What products and versions are affected by CVE-2026-66000?

CVE-2026-66000 affects frappe. Check the affected-versions section of this advisory for specific version ranges, vulnerable configurations, and compatibility information.

How do I fix or patch CVE-2026-66000?

The patch analysis section provides guidance on updating to patched versions, applying workarounds, and implementing compensating controls for frappe.

What is the CVSS score for CVE-2026-66000?

The severity rating and CVSS scoring for CVE-2026-66000 affecting frappe is documented in the vulnerability details section. Refer to the NVD entry for the current authoritative score.