SENAITE security hotfix 2026-06-02 (JSON API RCE)
Project description
Security hotfix for SENAITE LIMS, addressing a critical unauthenticated remote code execution in the SENAITE JSON API.
This package backports the two upstream fixes (senaite.core#2903 and senaite.core#2919) as runtime monkey patches so they can be installed on any SENAITE.CORE 2.x release (tested-stable from v2.0.0 through v2.6.0) without upgrading senaite.core itself. It follows the model of the Plone hotfixes (e.g. Products.PloneHotfix20210518).
What it fixes
The vulnerability is a chain of two independent flaws. Both must be closed; the hotfix applies both:
Eval injection (CWE-95) – senaite.core#2903. The JSON API parsed stringified RecordField / RecordsField payloads with the builtin eval(), executing attacker-controlled Python in the Zope worker. The hotfix shadows the module-global eval in the three affected modules
bika.lims.jsonapi (set_fields_from_request – the RCE sink)
senaite.core.browser.fields.record (RecordField.set)
senaite.core.browser.fields.records (RecordsField._to_dict)
with a safe parser based on ast.literal_eval. This is behaviourally equivalent to the merged parse_record_literal helper.
Missing authorization (CWE-862) – senaite.core#2919. The state-changing JSON API routes did not enforce the AccessJSONAPI permission, so anonymous and under-privileged callers could reach them. The hotfix wraps every state-changing route method
update, update_many
remove
doActionFor, doActionFor_many
getusers
so the AccessJSONAPI permission is checked before any mutation.
How it works
The patches are applied as an import side-effect of the SenaiteHotfix20260602 package. The package ships a z3c.autoinclude plugin (target = plone), so it is imported automatically at instance start-up – the same mechanism that loads any SENAITE add-on. Adding the egg and restarting the instance is all that is required; there is no GenericSetup profile to install, no ZODB migration, and nothing to configure.
The patches are idempotent and version-aware: on a senaite.core release that already carries the upstream fixes they are a harmless no-op (the eval sinks are gone, and the permission check simply runs a second, redundant time).
Installation
Add the egg to your instance and restart.
Buildout
[instance]
eggs +=
SenaiteHotfix20260602
If your deployment disables z3c.autoinclude auto-discovery, also load the ZCML explicitly:
[instance]
eggs +=
SenaiteHotfix20260602
zcml +=
SenaiteHotfix20260602
pip
$ pip install SenaiteHotfix20260602
Then restart the instance.
Verifying the fix
After restart, the instance log shows:
SenaiteHotfix20260602 installed (recordparsing, jsonapi_auth)
An anonymous call to a gated route (the advisory’s PoC) now returns a JSON error instead of executing code:
$ curl -s "http://localhost:8080/senaite/@@API/update?obj_uid=<uid>&RejectionReasons=__import__('os')..."
{"success": false, "error": true, ... "Unauthorized" ...}
Testing
The hotfix is tested against every senaite.core release from v2.0.0 to v2.6.0 by the tests GitHub Actions workflow (.github/workflows/tests.yml). The 2.x development branch is not tested because it already carries #2903 and #2919 (the hotfix is a no-op there). Each matrix cell:
checks out that senaite.core tag,
builds it with the tag’s own buildout.cfg (for the develop = . checkout under test and its structure), with the hotfix developed on top via test-senaite.cfg,
runs bin/test -s SenaiteHotfix20260602.
The dependency stack is pinned to what each release actually shipped with – the versions senaite.lims records, since senaite.core does not pin its own siblings:
Siblings (senaite.app.listing / spotlight / supermodel, senaite.impress, senaite.jsonapi) are not taken from their moving 2.x git branch. They are installed as released eggs pinned to the versions the matching senaite.lims release pins (contemporaneous with the core tag; they track the core tag except for v2.4.1, whose senaite.lims pins the siblings at 2.4.0).
Plone is re-pinned (via plone-kgs.cfg) to the Plone==5.2.x that the matching senaite.lims release requires, because a tag’s own buildout.cfg sometimes extends a slightly different Plone point release that would clash with the senaite.lims egg.
A few Python 2.7 fixup pins (magnitude, Pympler, et-xmlfile) are applied in test-senaite.cfg. These transitive dependencies are unpinned upstream and their newest releases dropped Python 2.7, so a from-scratch build of an old release today would otherwise pull a version that no longer builds. The values match the current senaite.core 2.x buildout.
The suite has two parts:
tests/test_recordparsing.py – version-independent unit tests of the safe literal parser (parses records, rejects code-execution payloads). Needs no SENAITE environment.
tests/test_jsonapi_auth.py – asserts the patches actually land on the installed senaite.core: eval is shadowed in all three modules, every state-changing route is gated, and the permission helper denies unauthorized callers.
Running it locally
One matrix cell can be reproduced locally (requires a Python 2.7 interpreter, a C toolchain and the libxml2 / libxslt headers):
scripts/run-tests-local.sh v2.6.0
To test against a senaite.core checkout you already have, copy the hotfix into it as SenaiteHotfix20260602/ and test-senaite.cfg alongside, drop in the matching Plone known-good-set, then build with the sibling pins for that release (see the table the workflow uses) and run the suite:
printf '[buildout]\nextends = https://dist.plone.org/release/5.2.15/versions.cfg\n' > plone-kgs.cfg
bin/buildout -c test-senaite.cfg \
versions:senaite.lims=2.6.0 \
versions:senaite.app.listing=2.6.0 \
versions:senaite.app.spotlight=2.6.0 \
versions:senaite.app.supermodel=2.6.0 \
versions:senaite.impress=2.6.0 \
versions:senaite.jsonapi=2.6.0
bin/test -s SenaiteHotfix20260602
The standalone parser tests need nothing but an interpreter:
python -m unittest SenaiteHotfix20260602.tests.test_recordparsing
Compatibility
SENAITE.CORE 2.0.0 – 2.6.0 (the patched code paths are identical across this range). Newer releases that already include the fixes are supported as a no-op.
Python 2.7 (Plone 5.2 / Zope 4). The code is Python 3 compatible should SENAITE move to it.
Credits
Security: Fix unauthenticated remote code execution chain in the JSON API (GHSA-jrw6-7x4q-w25j, CVE-2026-54569). Replaces dynamic evaluation of RecordField inputs with safe literal parsing (#2903), and enforces the AccessJSONAPI permission on the state-changing JSON API routes update, update_many, remove, doActionFor, doActionFor_many, and getusers (#2919).
Reported by Simon Weber, Volker Schönefeld and Chiara Fliegner, all of Machine Spirits UG (see their advisory). Independently reported by Jérémy Luyé-tanet of Synacktiv. Thanks for the responsible disclosure.
The upstream fixes bundled here were authored by Tyler Coatsworth (#2903) and Ramon Bartl (#2919).
License
GNU General Public License, version 2 (see docs/LICENSE.txt).
Changelog
1.0.0 (2026-06-02)
Initial release. Bundles the fixes for a critical unauthenticated remote code execution in the SENAITE JSON API:
senaite.core#2903: replace eval() with safe literal parsing for RecordField / RecordsField payloads (CWE-95).
senaite.core#2919: enforce the AccessJSONAPI permission on the state-changing JSON API routes (CWE-862).
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file senaitehotfix20260602-1.0.0.tar.gz.
File metadata
- Download URL: senaitehotfix20260602-1.0.0.tar.gz
- Upload date:
- Size: 17.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/1.15.0 pkginfo/1.8.3 requests/2.27.1 setuptools/44.1.1 requests-toolbelt/1.0.0 tqdm/4.64.1 CPython/2.7.18
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c4d8b29523d22cae2c0e8f850bf454c45d02ea84ae0e4626a5171046b8b37005
|
|
| MD5 |
b57762a16671a52884e7fd3333ebbaad
|
|
| BLAKE2b-256 |
da4a77ec1fb795cd202dcbda546287e8ccbcd918db8f7c3cf0618012a87ab2c6
|