pypi package report

Is senaite-core safe?

1 known vulnerability, worst severity CRITICAL.

// reach

0 direct dependencies

none carry a known advisory

    0 packages depend on it

    an advisory here reaches each of them

      Create a free accountfor every dependency path, dependent and what to upgrade
      // ai model usage

      No published models are known to use this package.


      cvss
      0.0
      critical

      severity out of 10

      epss
      0.00%
      medium

      chance of exploitation in 30 days, 67th percentile of all CVEs

      xyz score
      0.0
      medium

      CyberXYZ composite out of 10

      fig. 01 — GHSA-jrw6-7x4q-w25j, the advisory selected below

      // 1 advisories

      GHSA-jrw6-7x4q-w25j

      CRITICALCVE-2026-54569
      // 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, updatemany, remove, doActionFor, doActionFormany, 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 objuid is supplied, the route resolves the target through uidcatalog 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 bikasetup 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 setfieldsfromrequest (CWE-95). Once an object has been resolved, setfieldsfromrequest 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 writepermission 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 bikasetup 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.

      # 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:
      // cvss v3.1 vector

      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

      Checked 2026-10-23 at 18:51 UTC. The most recent advisory here was published 2026-08-26. Updated continuously from NVD, GHSA, OSV and CNA feeds.

      Think a verdict here is wrong? Tell us — we respond within 2 business days.