{"id":"CVE-2026-107841","aliases":["EUVD-2026-95937","GHSA-3hj7-6vmj-h8v4","PYSEC-2026-4246"],"status":"published","title":"pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents","description":"pacioli provides least-privilege governance and a governed agent broker for ERPNext. From version 0.9.6 until version 0.10.0, the pacioli-guard document-layer consent gate allows nested cancellation operations to ride any consent established by an enclosing governed act without checking whether the marker authorizes cancellation. A credential with API Key Scope.require_consent can submit a caller-controlled Sales Invoice or other supported document under a valid human-minted submit marker and reach Document.cancel() for a different pre-existing submitted document, bypassing the marker's document and act binding, single-use spend, and denial audit. The unauthorized cancellation can reverse the target document's ledger effect; principals without a consent-gated grant are not affected. This issue is fixed in version 0.10.0.","severity":{"version":"3.1","score":5.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00233,"percentile":0.14281,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-863"],"published":"2026-10-09T18:11:07Z","modified":"2026-10-28T02:30:21Z","fixes":[{"ecosystem":"PyPI","package":"pacioli-guard","fixed_in":["0.10.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"PyPI","package":"pacioli-guard","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.9.6"},{"fixed":"0.10.0"}]}]},{"source":"osv","ecosystem":"PyPI","package":"pacioli-guard","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.9.6"},{"fixed":"0.10.0"}]}],"versions":["0.9.6"]},{"source":"osv","ecosystem":"PyPI","package":"pacioli-guard","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.9.6"},{"fixed":"0.10.0"}]}],"versions":["0.9.6"]}],"products":[{"vendor":"john-broadway","product":"pacioli"}],"references":[{"url":"https://github.com/john-broadway/pacioli/security/advisories/GHSA-3hj7-6vmj-h8v4","tags":["x_refsource_CONFIRM","advisory","web"]},{"url":"https://github.com/john-broadway/pacioli/commit/f3c7219f5dde6050bd7921e0ac55afd02771250c","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/john-broadway/pacioli/releases/tag/guard-v0.10.0","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-107841","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-3hj7-6vmj-h8v4","tags":["advisory"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/107xxx/CVE-2026-107841.json","tags":["advisory"]},{"url":"https://github.com/john-broadway/pacioli","tags":["package"]},{"url":"https://pypi.org/project/pacioli-guard","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-107841","title":"pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents","description":"pacioli provides least-privilege governance and a governed agent broker for ERPNext. From version 0.9.6 until version 0.10.0, the pacioli-guard document-layer consent gate allows nested cancellation operations to ride any consent established by an enclosing governed act without checking whether the marker authorizes cancellation. A credential with API Key Scope.require_consent can submit a caller-controlled Sales Invoice or other supported document under a valid human-minted submit marker and reach Document.cancel() for a different pre-existing submitted document, bypassing the marker's document and act binding, single-use spend, and denial audit. The unauthorized cancellation can reverse the target document's ledger effect; principals without a consent-gated grant are not affected. This issue is fixed in version 0.10.0.","status":"published","published":"2026-10-09T18:11:07.226Z","modified":"2026-10-09T18:41:50.092Z","cvss":[{"version":"3.1","score":5.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T13:45:14.072357Z"}],"euvd":[{"id":"EUVD-2026-95937","description":"pacioli provides least-privilege governance and a governed agent broker for ERPNext. From version 0.9.6 until version 0.10.0, the pacioli-guard document-layer consent gate allows nested cancellation operations to ride any consent established by an enclosing governed act without checking whether the marker authorizes cancellation. A credential with API Key Scope.require_consent can submit a caller-controlled Sales Invoice or other supported document under a valid human-minted submit marker and reach Document.cancel() for a different pre-existing submitted document, bypassing the marker's document and act binding, single-use spend, and denial audit. The unauthorized cancellation can reverse the target document's ledger effect; principals without a consent-gated grant are not affected. This issue is fixed in version 0.10.0.","status":"published","published":"2026-10-09T18:11:07Z","modified":"2026-10-09T18:41:50Z","cvss":[{"version":"3.1","score":5.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"updated_at":"2026-11-02T17:27:24.64208Z"}],"ghsa":[{"id":"GHSA-3hj7-6vmj-h8v4","title":"pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents","description":"### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep.","status":"published","published":"2026-10-09T20:42:51Z","modified":"2026-10-09T20:42:53Z","cvss":[{"version":"3.1","score":5.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T14:41:31.175024Z"}],"nvd":[{"id":"CVE-2026-107841","description":"pacioli provides least-privilege governance and a governed agent broker for ERPNext. From version 0.9.6 until version 0.10.0, the pacioli-guard document-layer consent gate allows nested cancellation operations to ride any consent established by an enclosing governed act without checking whether the marker authorizes cancellation. A credential with API Key Scope.require_consent can submit a caller-controlled Sales Invoice or other supported document under a valid human-minted submit marker and reach Document.cancel() for a different pre-existing submitted document, bypassing the marker's document and act binding, single-use spend, and denial audit. The unauthorized cancellation can reverse the target document's ledger effect; principals without a consent-gated grant are not affected. This issue is fixed in version 0.10.0.","status":"published","published":"2026-10-09T18:17:06.11Z","modified":"2026-10-16T17:17:04.113Z","cvss":[{"version":"3.1","score":5.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N","level":"medium","assessor":"secondary"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T14:12:55.473789Z"}],"osv":[{"id":"CVE-2026-107841","title":"pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents","description":"pacioli provides least-privilege governance and a governed agent broker for ERPNext. From version 0.9.6 until version 0.10.0, the pacioli-guard document-layer consent gate allows nested cancellation operations to ride any consent established by an enclosing governed act without checking whether the marker authorizes cancellation. A credential with API Key Scope.require_consent can submit a caller-controlled Sales Invoice or other supported document under a valid human-minted submit marker and reach Document.cancel() for a different pre-existing submitted document, bypassing the marker's document and act binding, single-use spend, and denial audit. The unauthorized cancellation can reverse the target document's ledger effect; principals without a consent-gated grant are not affected. This issue is fixed in version 0.10.0.","status":"published","published":"2026-10-09T18:11:07.226Z","modified":"2026-10-28T02:30:21.882061548Z","cvss":[{"version":"3.1","score":5.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T15:44:54.933106Z"},{"id":"GHSA-3hj7-6vmj-h8v4","title":"pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents","description":"### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep.","status":"published","published":"2026-10-09T20:42:51Z","modified":"2026-10-15T14:55:55.450927331Z","cvss":[{"version":"3.1","score":5.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"cwes":["CWE-863"],"updated_at":"2026-11-02T15:46:35.747385Z"},{"id":"PYSEC-2026-4246","title":"pacioli: A submit consent marker licensed cancellation of caller-named pre-existing documents","description":"### Impact\n\n`pacioli-guard`'s document-layer consent gate requires a human-minted, single-use, document-bound and **act-bound** marker before a credential carrying `API Key Scope.require_consent` may submit or cancel a document. Because ERPNext performs further document writes as a consequence of a governed act, nested acts are allowed to \"ride\" the consent established by the enclosing act instead of needing a marker of their own.\n\nThe ride predicate returned `true` for **every** cancel, regardless of which act the enclosing marker authorised. Riding never reaches `consent_verdict`, which is where the marker-to-act binding is enforced. The result: any `Document.cancel()` performed inside an act that had established consent reached `docstatus = 2` with no marker, no act-binding check, no single-use spend, and **no denial audit row**.\n\nThis is reachable with the exact grant an operator is documented to give the broker, and the cancelled document's identity is caller-controlled:\n\n- `Sales Invoice.on_submit` (`sales_invoice.py:507`) calls `process_asset_depreciation()` unconditionally, reaching `depreciate_asset_on_sale` (`:1508-1516`).\n- That iterates the invoice's item rows and calls `frappe.get_doc(\"Asset\", d.asset)`. `Sales Invoice Item.asset` is a plain writable `Link` with no `read_only` and no `fetch_from`, so the caller supplies it in the request body. The validation that would constrain it sits behind `if d.is_fixed_asset:` (`:428-429`), a server-set field.\n- The chain reaches `depreciation.py:481` and then `asset_depreciation_schedule.py:215-217`, which calls `current_schedule.cancel()` on a `docstatus == 1`, submittable `Asset Depreciation Schedule` **through the document lifecycle** (only `should_not_cancel_depreciation_entries` is set, not `ignore_validate`, so `before_cancel` does fire and the ride is what admitted it).\n\nSo one marker authorising `submit Sales Invoice X` cancelled a submitted document the human was never shown and never approved. Cancelling such a document reverses its ledger effect.\n\nThe same shape exists behind wider grants, for example `Unreconcile Payment.on_submit` (`unreconcile_payment.py:59-64`), which walks a child table the caller fills and reaches `accounts/utils.py:857`/`:859` `gain_loss_je.cancel()` on submitted Journal Entries.\n\n### Who is affected\n\nOnly sites that had opted into consent gating: a credential must hold an `API Key Scope` with `require_consent` set. `pacioli-guard` is inert for principals without such a grant, and the credential-scoping floor (`auth_hooks`) is unaffected by this issue.\n\nThe document-layer consent gate was first published in 0.9.6, which is the only released version in the affected range.\n\n### Patches\n\nFixed in **0.10.0**. The ride now discriminates on the **enclosing act**: an undo may cascade into further undos, but a submit may not cascade into the cancellation of a document that already has a name, because that is an act a human could have been asked to approve. The custody stamp carries the act it was established for rather than a bare boolean, which is the state the vulnerable code lacked.\n\n**Upgrading changes behavior.** Any ERPNext flow where a submit cascades into a lifecycle cancel now requires a consent marker for that cancel as well as for the act itself. In ERPNext v16 that includes asset sale, partial-quantity asset sale, a credit note against an asset sale, Asset Repair capitalization, Asset Shift Allocation, Asset Value Adjustment, and `Unreconcile Payment`. To make that possible, the `X-Pacioli-Consent` header now accepts several markers separated by whitespace or commas; each remains bound to one document and one act, requires a different minter, and is spent exactly once.\n\n### Workarounds\n\nOn 0.9.6, remove `require_consent` from affected grants and rely on the credential-scoping floor alone, or scope governed credentials so they cannot submit documents whose controllers cancel other documents (for ERPNext slice-one, `Sales Invoice` with a populated item-row `asset` link is the known path). Neither is a substitute for upgrading.\n\n### Residual, stated\n\nUnder a governed **cancel**, a cascaded cancel of a pre-existing document still rides. That is load-bearing for undo (an ordinary invoice cancel makes ERPNext cancel the credit/debit notes and journals it generated, via `accounts_controller.py:2001-2005`) and cannot be narrowed without a signal that a cascaded cancel is a consequence of the enclosing document specifically. That justification does not describe everything the residual admits: at least one instance is caller-steered in the same shape as the issue fixed here, `Asset Repair.on_cancel` (`asset_repair.py:215-222`), which cancels a `Serial and Batch Bundle` named by a Link the caller fills in a child table. `pacioli-guard` also does not see writes that set `flags.ignore_validate` or that skip the document lifecycle entirely (raw SQL, `db_update`/`db_set` field writes); those residuals are published in the project's own documentation and are unchanged by this advisory.\n\n### Credit\n\nFound by an internal adversarial review on 2026-07-28 that traced the predicate against frappe 16.28.0 and ERPNext v16 source rather than the project's own documentation. The prior code comment asserted that no such lever was known; that assertion had been written without the corresponding source sweep.","status":"published","published":"2026-10-15T10:22:55.058463Z","modified":"2026-10-15T14:45:12.948309515Z","cvss":[{"version":"3.1","score":5.7,"vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:N","level":"medium"}],"updated_at":"2026-11-02T15:52:57.671918Z"}]}}
