Senaite LIMS unauthenticated eval injection — operator validation¶
Date reviewed: 2026-08-27
Advisory: GHSA-JRW6-7X4Q-W25J / CVE-2026-54569
Severity: Critical (CVSS 9.8)
Affected: senaite.core (Zope-based LIMS) prior to the patch referenced by the advisory
Boundary class: unauthenticated JSON API write route → Python eval() sink
What is durable here¶
Senaite is a Zope-based Laboratory Information Management System that frequently runs on public management subnets, DMZs, or behind a single WAF rule. The advisory describes a two-request anonymous chain:
- Missing
AccessJSONAPIgate on the JSON API write route. The/@@API/updateroute runs aneval()on attacker-controlled input before any permission check fires. The siblingcreate.pyroute does enforce the permission; theupdate.pyroute does not. eval()sink on a write-accessible field. Even after the permission gate is restored, an authenticated user with write access to aRecordsFieldcan still reach the sameeval()sink. Either fix alone does not close the unauthenticated chain.
The operator value is the unauthenticated eval() on a JSON API route — a rare, high-yield primitive on a Zope stack. Most Zope applications sit behind the same Zope instance as other products, so a working chain here often gives a foothold on a host that also runs lab data, sample management, and sometimes internal integrations.
Validation workflow (authorized lab / customer-approved)¶
Recon¶
- Enumerate Zope routes on the target host. Senaite LIMS typically exposes
/@@API/create,/@@API/update, and a handful of other JSON API routes. Theupdateroute is the interesting one. - Confirm the product is
senaite.core(not a fork). The advisory's affected range is specific tosenaite.core. - Note the Zope instance version. Older Zope instances have additional unauthenticated management surfaces.
Proof of unauth reachability (no code execution)¶
- Send an authenticated request to
/@@API/updatewith a valid session token and a benign field update. Confirm it works. - Repeat the same request without a session token (or with an anonymous cookie jar). If the route returns
200and applies the update, theAccessJSONAPIgate is missing. - Do not attempt code execution. The proof is the route-level permission differential, not the
eval()payload.
If the gate is present¶
- An authenticated account with
RecordsFieldwrite access can still reach theeval()sink. Validate by: - Obtaining a low-privilege account with write access to a
RecordsField(ask the customer for a throwaway account). - Sending an update that sets the field to a benign string.
- Confirming the update is applied without an
AccessJSONAPIpermission error. Theeval()sink is present but the chain requires both flaws; record this as a conditional finding.
Negative evidence to record¶
- A
403or401on the anonymous/@@API/updaterequest means the gate is present. - A
400with a permission error means the gate is present but theeval()sink may still be reachable by authenticated users.
Reporting heuristic¶
- Lead with the unauthenticated
eval()— it is the critical finding. The authenticated variant is a secondary finding that requires two flaws to chain. - State the Zope instance version and whether the
update.pyroute is the one atsrc/bika/lims/jsonapi/update.pyor a fork. - Do not publish
eval()payloads. The proof is the route-level permission differential. - If the target is a fork of
senaite.core, note the fork name and version; the advisory range may not apply.
Safety constraints¶
- Do not execute arbitrary code on the target.
- Do not modify lab data, sample records, or workflow state.
- Use throwaway accounts only.
- Do not test against production LIMS data.