All Advisories

SENAITE.CORE

Unauthenticated RCE via JSON API RecordsField Eval

The SENAITE JSON API update route omits the AccessJSONAPI permission check that its sibling create route enforces, leaving it reachable to anonymous callers on a stock Plone site. Once a target is resolved, set_fields_from_request runs eval() on the raw request value of any RecordsField or RecordField before the field mutator's permission check. A two-request anonymous chain executes arbitrary Python in the Zope worker process.

SeverityCriticalCVSS 9.8CVSS 3.1 VectorAV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HCWECWE-95 (Eval Injection)ProductSENAITE.COREAffected VersionsAll 2.x releases, 2.0.0 through 2.6.0 (both root causes predate the SENAITE fork and remain on the current 2.x development tip)Fixed InNo patched release available at time of publication. See the mitigation and defender's checklist below.CVECVE-2026-54569GHSAGHSA-jrw6-7x4q-w25j

Description

Flaw 1: missing authorization on the `update` route. The @@API view is registered by plone.jsonapi.core at permission="zope2.View", which a stock Plone site grants to Anonymous. The create route guards itself with an explicit AccessJSONAPI permission check; update, update_many, remove, doActionFor, doActionFor_many, and getusers do not.

bika/lims/jsonapi/create.py:179-182 (the check that update omits)

if not getSecurityManager().checkPermission(
"senaite.core: Access JSON API", parent):
raise Unauthorized("You don't have permission ...")

View source →

Flaw 2: the eval sink. After the route resolves the target object, set_fields_from_request iterates the request fields. For a RecordsField or RecordField, it runs eval() on the raw request string, before the field mutator and its write_permission check execute. The eval runs in the Zope worker with full Python builtins available.

bika/lims/jsonapi/__init__.py:237-245

elif fieldtype in ['senaite.core.browser.fields.records.RecordsField',
'senaite.core.browser.fields.record.RecordField']:
try:
value = eval(value)
except Exception:
logger.warning(
"JSONAPI: " + fieldname + ": Invalid "
"JSON/Python variable")
return []

View source →

The chain. The bika_setup object is anonymous-readable on a stock Plone site, exposes two RecordsField-typed fields (RejectionReasons and IDFormatting), and its UID is published by Plone's standard anonymous @@uuid view. An anonymous attacker fetches the UID at /<site>/bika_setup/@@uuid, then POSTs it to /<site>/@@API/update with a field value that reaches the eval sink. No Authorization, Cookie, or CSRF token is required.

The update route wraps the write in a transaction savepoint, so the ZODB changes roll back when the mutator later fails on the unexpected value. The Python side effects of the eval (subprocess execution, outbound network calls, file I/O outside ZODB) have already happened and are not reverted, so the failed transaction does not undo the code execution.

Impact

  • Unauthenticated remote code execution: a single anonymous HTTP request executes arbitrary Python in the Zope worker process, with full read/write access to the ZODB (laboratory records, including patient data in clinical deployments), filesystem access on the container's data volume, and outbound network egress. Write access includes silently altering or deleting individual analysis results that clinicians rely on.
  • Persistence: code execution gives direct access to acl_users (the Plone PAS user folder) for creating administrator accounts, and the ZMI at /manage is exposed in the upstream Docker image.
  • The transaction rollback is not a mitigation: the failed mutator reverts ZODB writes, but the subprocess, network, and file-I/O side effects of the eval have already executed.

Mitigation

No patched release exists at time of publication, so both root causes must be addressed at the deployment or source level. Restrict anonymous access to the @@API view (for example, require authentication for /@@API/ at the reverse proxy) until an upstream fix ships. At the source level, add the AccessJSONAPI permission check to the top of every state-changing route in src/bika/lims/jsonapi/ (mirroring the existing check in create.py), and replace the eval() on RecordField/RecordsField values in set_fields_from_request with json.loads(): the stored shape is a JSON-compatible dict or list of dicts, so JSON parsing is sufficient and removes the code-execution primitive.

Defender's Checklist

  • Block anonymous access to the JSON API.

    Until a patched release ships, require authentication for /@@API/ at the reverse proxy, or otherwise restrict the endpoint to trusted networks. The chain needs no credentials, so network exposure is the immediate risk.

  • Replace eval() with json.loads() on RecordField/RecordsField values.

    The same eval() pattern also lives in the field setters at src/senaite/core/browser/fields/record.py and records.py. Switch all three to json.loads() with a try/except returning empty on failure.

  • Add the AccessJSONAPI check to every state-changing route.

    At minimum update, update_many, remove, doActionFor, doActionFor_many, and getusers are reachable to anonymous without it. Audit every IRouteProvider in src/bika/lims/jsonapi/configure.zcml.

  • Re-enable Plone's CSRF protection.

    The audited release ships ISenaiteCore(IDisableCSRFProtection), which disables plone.protect's automatic CSRF write-detection for the SENAITE browser layer. Removing the inheritance does not affect this unauthenticated chain but closes several authenticated CSRF chains.

Severity Reasoning

AV:NThe endpoint is reached over HTTP; any network client that can reach the SENAITE instance can send the request.AC:LNo timing or environmental conditions. The @@API view is anonymous-reachable and the eval sink runs unconditionally for RecordsField/RecordField values.PR:NNo credentials are required. The chain works with no Authorization, Cookie, or CSRF token against a stock deployment.UI:NThe attacker drives both requests directly; no victim action is involved.S:UCode execution stays within the Zope worker's security authority; the vulnerable component and the impacted resources share the same scope.C:HArbitrary Python grants full read access to the ZODB, including laboratory records and patient data in clinical deployments.I:HArbitrary Python grants full write access to the ZODB and the ability to create administrator accounts via acl_users.A:HCode execution in the worker process allows denial of service against the application and its data store.

References

How We Can Help

Who We Are

The security researchers behind this advisory.

Dr. Simon Weber Profile

Dr. rer. nat. Simon Weber

Senior Pentester & MedSec Researcher

I evaluate your SaMD with the same industry-defining security insight I contributed to the BAK MV for the revision of the B3S standard.

  • PhD on Hospital Cybersecurity
  • Critical vulnerabilities found in hospital systems
  • Alumni of THB MedSec Research Group
  • gematik Security Hero
Volker Schönefeld Profile

Dipl.-Inf. Volker Schönefeld

Senior Application Security Expert

As a former CTO and developer turned pentester, I work alongside your team to uncover vulnerabilities and find solutions that fit your architecture.

  • 20+ years as CTO, 50M+ app downloads
  • Architected and secured large-scale IoT fleets
  • Certified Web Exploitation Specialist
  • gematik Security Hero

Looking for a Penetration Test?

Machine Spirits specializes in security assessments for medical devices and healthcare IT. From MDR penetration testing to C5 cloud compliance, we help MedTech companies meet regulatory requirements.