Skip to main content

SenaiteHotfix20260602

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:

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

  2. 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:

  1. checks out that senaite.core tag,

  2. 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,

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

Metadata

Release files for SenaiteHotfix20260602 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for SenaiteHotfix20260602 1.0.0
File Size Uploaded
senaitehotfix20260602-1.0.0.tar.gz 17.9 kB Details

Release files / senaitehotfix20260602-1.0.0.tar.gz

Download URL senaitehotfix20260602-1.0.0.tar.gz
Size 17.9 kB
Tags Source
SHA-256 checksum
How to use checksums
c4d8b29523d22cae2c0e8f850bf454c45d02ea84ae0e4626a5171046b8b37005
BLAKE2b-256 checksum
How to use checksums
da4a77ec1fb795cd202dcbda546287e8ccbcd918db8f7c3cf0618012a87ab2c6
Upload date
Uploaded using Trusted Publishing?
What is 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

Release history Release notifications | RSS feed

This release

1.0.0 This release

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page