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).
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
|