The archive/CVE-2019-7164
sqlalchemy/sqlalchemyDisclosed 2019

CVE-2019-7164:sqlalchemy: SQL injection

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

Historical recordrel_1_2_18 (affected: all versions before 1.3.0b3; the 1.2.x line -- including 1.2.18 -- never received the backport)
WeaknessCWE-89 · SQL injection
ImpactA sql injection in sqlalchemy, reachable from attacker-controlled input.
Read the reconstruction
01What happened

Attacker-controlled text in order_by()/group_by() reaches SQL string assembly (SQLAlchemy < 1.3.0b3).

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-2019-7164
Vulnerable target
rel_1_2_18 (affected: all versions before 1.3.0b3; the 1.2.x line -- including 1.2.18 -- never received the backport) · vulnerable
Prior knowledge
CVE-2019-7164 · rel_1_2_18 (affected: all versions before 1.3.0b3; the 1.2.x line -- including 1.2.18 -- never received the backport) is vulnerable
then
02Lachesis run
Verified captured run

Independent re-detection

The experiment graph was built around the vulnerable sql module (lib/sqlalchemy/sql) at release ref rel_1_2_18, rather than starting from arbitrary SQLAlchemy source. We did not point the tool at the CVE. Enumeration ran over the whole candidate registry (31 constructors, every family) and 569 candidates were surfaced; the injection.query.escaping family fired 41 times on its own alongside the memory-lifetime, navigation, filesystem and deserialize families. Every field under sast_output is the enumerator's own emission (candidate capsule plus provenance cone); everything under adjudication is my reading of the source and fix and is labelled as such. Note honestly: the tool's top injection.query candidate anchors on self.connection.execute(CreateSequence(sequence)) in ddl.py, which is a class-correct SQL-execution sink but NOT the order_by/group_by textual-coercion path that is the actual CVE-2019-7164 root cause.

Seeded inputs
None
Run timestamp
2026-09-02T13:29:35Z
Evidence artifact
sqlalchemy_sql.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-2019-7164
Attacker-controlled text in order_by()/group_by() reaches SQL string assembly (SQLAlchemy < 1.3.0b3).
01Sourceattacker-controlled input
02Sinkno validation that an order_by()/group_by() string resolves to a real column label before it is rendered as SQL
03Missing guardno validation that an order_by()/group_by() string resolves to a real column label before it is rendered as SQL
$ lachesis sources_of → reaches → guard_dominance
sqlalchemy/sqlalchemy · rel_1_2_18 (affected: all versions before 1.3.0b3; the 1.2.x line -- including 1.2.18 -- never received the backport) · 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.

Repositorysqlalchemy/sqlalchemy
Revisionrel_1_2_18 (affected: all versions before 1.3.0b3; the 1.2.x line -- including 1.2.18 -- never received the backport) · vulnerable
EngineLachesis

Captured runThis is a real replay of Lachesis over the graph we built for this case at ref rel_1_2_18. Every command below was run against the sqlalchemy_sql.kuzu graph and every result is the output captured on that run (elapsed 33.9s). We did not point the tool at a file or a family. The hunt enumerated the whole taxonomy first (569 candidates) and the injection.query.escaping family (41 hits) came out of that blind. No SQL payload was seeded; the graph was built from the source alone and the SQL-injection family was discovered from graph structure, so the mode is independent-redetection.

01 · load_graph01 / 06
Lachesis ran
load_graph --path sqlalchemy_sql.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. injection is one row here, not something we aimed at.

Captured resultCaptured · load_graph
raw captured outputload_graph
[lachesis-mcp] loaded the graph (rel_1_2_18); pass2 published 38327 nodes and 129944 edges

CANDIDATE_CENSUS
taxonomy (8): lifecycle, memory, injection, navigation, object-integrity, filesystem, crypto-config, resource
constructors (31): complete_for_observable_graph=True
lib/sqlalchemy/sql/ddl.pyline 0
Code location returned for this evidence step.
The repair

A small line with a large consequence.

unresolved string arguments were silently coerced to text() and emitted as raw SQL rather than rejected, so untrusted ordering text became executable SQL.

Before
no validation that an order_by()/group_by() string resolves to a real column label before it is rendered as SQL
After the patch
// always reject unresolved string coercion (raise CompileError/ArgumentError) instead of rendering raw SQL -- fixed in 1.3.0b3, commit 30307c4, not backported to 1.2.x.
Sources & further reading

The original record.

Read the historical advisory and vulnerability record behind this reconstruction.

Open the Lachesis quickstart