Skip to main content

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:

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

Project details


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.

Supported by

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