The archive/CVE-2021-23727
celery/celeryDisclosed 2021

CVE-2021-23727:celery: Stored OS command / code injection via untrusted backend res

A reconstruction of the flaw, and the evidence path Lachesis follows to make it visible.

Historical recordv5.2.1 (affected: all versions < 5.2.2)
WeaknessCWE-77 → CWE-78 · Stored OS command / code injection via untrusted backend res
ImpactA stored os command / code injection via untrusted backend res in celery, reachable from attacker-controlled input.
Read the reconstruction
01What happened

Tampered task-failure metadata triggers unsafe exception reconstruction when the result is read.

That is the shape of the failure. The rest of this case file follows the evidence behind it.

Separate the record from the run

What was known.
What Lachesis established.

01Historical record

Known before Lachesis ran

The CVE and vulnerable release were selected from public history. We are not claiming novel discovery.

CVE
CVE-2021-23727
Vulnerable target
v5.2.1 (affected: all versions < 5.2.2) · vulnerable
Prior knowledge
CVE-2021-23727 · v5.2.1 (affected: all versions < 5.2.2) is vulnerable
then
02Lachesis run
Verified captured run

Independent re-detection

The experiment graph was built around the celery package at the vulnerable release v5.2.1, not pointed at the CVE. Blind enumeration ran over the whole candidate registry (31 constructors, every family) with the pass-3 guard-differential ranker ordering leads. The expected injection.exec family surfaced on its own (19 injection.exec.escaping obligations) alongside lifecycle, navigation, filesystem and deserialize families. Everything under sast_output is the enumerator's own emission (candidate capsule plus sources_of value-flow cone); everything under adjudication and trace narrative is my reading of the verbatim source and the official fix and is labelled as such. Honesty note carried through: the family fired but the highest-ranked sink is os.execv in the worker self-reload path, which is not the CVE mechanism -- see what_the_tool_did_NOT_prove.

Seeded inputs
None
Run timestamp
2026-09-02T13:27:48Z
Evidence artifact
celery_v5.2.1.kuzu

The commands and outputs below come from this recorded Lachesis run.

The reconstruction, in one frame

What Lachesis reconstructed.

One historical repository. Four captured queries. A complete source-to-sink argument.

Lachesis Casefiles · Unbound ComputeVerified runCVE-2021-23727
Tampered task-failure metadata triggers unsafe exception reconstruction when the result is read.
01Sourcetask-failure metadata read from the result backend and deserialized
02Sinkos.execv(sys.executable, [sys.executable] + sys.argv)
03Missing guardexception class from backend metadata imported and instantiated with no trust/type check
$ lachesis sources_of → reaches → guard_dominance
celery/celery · v5.2.1 (affected: all versions < 5.2.2) · vulnerableunboundcompute.com
This was known history. Your code is not.

Lachesis follows the same evidence path through unfamiliar repositories.

The Lachesis reconstruction

Here is how the path becomes visible.

These are the recorded questions Lachesis asked of the historical vulnerable code, followed by the raw result and source location each query returned.

Repositorycelery/celery
Revisionv5.2.1 (affected: all versions < 5.2.2) · vulnerable
EngineLachesis

Captured runReal replay of Lachesis over the graph built for the celery v5.2.1 case. Every command was run against that graph and every result is the captured output. The tool was not pointed at a file or a family; the hunt enumerated the whole taxonomy first and the injection.exec family came out of that. No message or backend payload was seeded, so the mode is independent-redetection. The reconstruction faithfully records that the top exec sink is the worker self-reload os.execv, not the CVE's exception-reconstruction path.

01 · load_graph01 / 05
Lachesis ran
load_graph --path celery_v5.2.1.kuzu --profile all && candidate_census

Load the graph and list every bug family

Load the graph and list every family with nothing chosen ahead of time. All 8 domains and 31 constructors are reported. injection.exec is one row among many, not a target.

Captured resultCaptured · load_graph
raw captured outputload_graph
loaded graph (celery v5.2.1, 103075 nodes / 309704 edges); taxonomy: 8 domains, 31 constructors; census complete for observable graph; 776 candidates enumerated across families.
celery/apps/worker.pyline 0
Code location returned for this evidence step.
The repair

A small line with a large consequence.

the pre-5.2.2 path trusts backend content and reconstructs whatever class the metadata names, so a poisoned backend yields code execution on the reader.

Before
exception class from backend metadata imported and instantiated with no trust/type check
After the patch
// validate that only genuine Exception subclasses are reconstructed and reject untrusted class references (fixed in 5.2.2, commit 1f7ad7e6).
Sources & further reading

The original record.

Read the historical advisory and vulnerability record behind this reconstruction.

Open the Lachesis quickstart