Skip to main content

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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

senaitehotfix20260602-1.0.0.tar.gz (17.9 kB view details)

Uploaded Source

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

Hashes for senaitehotfix20260602-1.0.0.tar.gz
Algorithm Hash digest
SHA256 c4d8b29523d22cae2c0e8f850bf454c45d02ea84ae0e4626a5171046b8b37005
MD5 b57762a16671a52884e7fd3333ebbaad
BLAKE2b-256 da4a77ec1fb795cd202dcbda546287e8ccbcd918db8f7c3cf0618012a87ab2c6

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

1 file

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page