The archive/CVE-2020-11652
saltstack/saltDisclosed 2020

CVE-2020-11652:salt: Path traversal

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

Historical recordv3000 (affected: < 2019.2.4, and 3000 < 3000.2)
WeaknessCWE-22 → CWE-20 · Path traversal
ImpactA path traversal in salt, reachable from attacker-controlled input.
Read the reconstruction
01What happened

A salt-api config.update_config call with ../ in file_name escapes the master config directory.

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-2020-11652
Vulnerable target
v3000 (affected: < 2019.2.4, and 3000 < 3000.2) · vulnerable
Prior knowledge
CVE-2020-11652 · v3000 (affected: < 2019.2.4, and 3000 < 3000.2) is vulnerable
then
02Lachesis run
Verified captured run

Independent re-detection

The experiment graph was built around the vulnerable salt/wheel subtree of salt-3000 (resolved_ref v3000), not from arbitrary Salt source, and the tool was never pointed at the CVE. Enumeration ran over the whole candidate registry (every family, no family hardcoded); the filesystem.path.containment family fired blind on 6 wheel-module sites alongside the lifecycle and injection families, and Lachesis traced the destination path back through os.path.join into __opts__. Every field under sast_output is the enumerator's own emission (candidate capsule plus the sources_of value-flow cone); everything under adjudication is my reading of the verbatim vulnerable source and the official fix, and is labelled as such.

Seeded inputs
None
Run timestamp
2026-09-02T13:26:11Z
Evidence artifact
salt_wheel.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-2020-11652
A salt-api config.update_config call with ../ in file_name escapes the master config directory.
01Sourcedir_path = os.path.join(__opts__['config_dir'], os.path.dirname(__opts__['default_include']))
02Sinkos.makedirs(dir_path, 0o755)
03Missing guardfile_path = os.path.join(dir_path, file_name) with salt.utils.files.fopen(file_path, 'w') as fp_:
$ lachesis sources_of → reaches → guard_dominance
saltstack/salt · v3000 (affected: < 2019.2.4, and 3000 < 3000.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.

Repositorysaltstack/salt
Revisionv3000 (affected: < 2019.2.4, and 3000 < 3000.2) · vulnerable
EngineLachesis

Captured runThis is a real replay of Lachesis over the graph we built for this case from the salt/wheel subtree of salt-3000. Every command below was run against that graph and every result is the output captured on that drive. We did not point the tool at a file or a family. The hunt listed the whole taxonomy first and the path-containment sinks came out of that. No request or payload was seeded; the graph was built from the source alone and the finding was discovered from graph structure, so the mode is independent-redetection.

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

Load the graph and list every bug family

First we load the graph and ask the tool to list every bug family it knows, with nothing chosen ahead of time. It reports all 8 domains and 31 sink constructors over the wheel subtree and enumerates 262 candidates. filesystem.path.containment is just one family among these, not something we aimed at.

Captured resultCaptured · load_graph
raw captured outputload_graph
[lachesis-mcp] loaded the graph (published 957 nodes and 2477 edges); overlay: derived edges on demand, per cone

CANDIDATE_CENSUS
move: candidate_census
taxonomy (8 domains): lifecycle, memory, injection, navigation, object-integrity, filesystem, crypto-config, resource
constructors (31): complete_for_observable_graph=True
n_candidates: 262
salt/wheel/config.pyline 0
Code location returned for this evidence step.
The repair

A small line with a large consequence.

the resolved file_path (and the makedirs dir_path) is never validated to stay inside the intended master.d directory; a file_name with ../ escapes it.

Before
file_path = os.path.join(dir_path, file_name)
with salt.utils.files.fopen(file_path, 'w') as fp_:
After the patch
// canonicalise and bound every path against its intended root before use (salt.utils.verify.clean_path, added in 3000.2 / 2019.2.4, commit cce7abad).
Sources & further reading

The original record.

Read the historical advisory and vulnerability record behind this reconstruction.

Open the Lachesis quickstart