The archive/CVE-2022-29078
mde/ejsDisclosed 2022

CVE-2022-29078:ejs: Server-side template injection / code injection , ref.

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

Historical recordv3.1.6 (affected: < 3.1.7)
WeaknessCWE-94 → CWE-74 · Server-side template injection / code injection , ref.
ImpactA server-side template injection / code injection , ref. in ejs, reachable from attacker-controlled input.
Read the reconstruction
01What happened

Attacker-influenced render options (e.g. outputFunctionName) reach ejs template compilation as code.

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-2022-29078
Vulnerable target
v3.1.6 (affected: < 3.1.7) · vulnerable
Prior knowledge
CVE-2022-29078 · v3.1.6 (affected: < 3.1.7) is vulnerable
then
02Lachesis run
Verified captured run

Independent re-detection

The experiment graph was built around the vulnerable ejs library tree at release v3.1.6 (lib scope), rather than starting from arbitrary source. We did not point the tool at the CVE. Enumeration ran over the whole candidate registry (default registry spans every family) with the pass-3 ranker ordering leads; the injection family surfaced on its own alongside the lifecycle, navigation, object-integrity and resource families, and 12 injection.exec.escaping candidates fired across lib/ejs.js -- including the template compile entrypoints (exports.compile @379, exports.render @413) and the two `new Function(...)` constructors (@107, @648) that assemble and execute the generated template function. The top-ranked capsule anchored on a RegExp.prototype.exec call in getIncludePath (line 158), a catalogued injection.exec sink that is a look-alike, not the SSTI itself. Every field under sast_output is the enumerator's own emission (candidate capsule plus sources_of value-flow cone); everything under adjudication is my reading of the source and fix and is labelled as such.

Seeded inputs
None
Run timestamp
2026-09-02T13:32:54Z
Evidence artifact
ejs-3.1.6.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-2022-29078
Attacker-influenced render options (e.g. outputFunctionName) reach ejs template compilation as code.
01Sourceoptions.outputFunctionName = opts.outputFunctionName;
02Sinkprepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
03Missing guardif (opts.outputFunctionName) { prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n'; }
$ lachesis sources_of → reaches → guard_dominance
mde/ejs · v3.1.6 (affected: < 3.1.7) · 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.

Repositorymde/ejs
Revisionv3.1.6 (affected: < 3.1.7) · vulnerable
EngineLachesis

Captured runThis is a real replay of Lachesis over the graph built for this case from ejs v3.1.6 (lib scope). Every command below was run against that graph and every result is 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 injection family came out of that (12 injection.exec.escaping candidates out of 428). No request or payload was seeded; the graph was built from source alone and the injection leads were discovered from graph structure, so the mode is independent-redetection. Elapsed 5.7s.

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

Load the graph and list every bug family

First we load the graph and enumerate blind over the whole taxonomy, with no family chosen ahead of time. 428 candidates come back across the domains; injection.exec.escaping is one family among lifecycle, navigation, object-integrity, resource and filesystem.

Captured resultCaptured · load_graph
raw captured outputload_graph
graph loaded (1156 nodes, 3371 edges, typescript-compiler-api frontend); taxonomy enumerable across all domains; 428 candidates enumerated blind.
lib/ejs.jsline 0
Code location returned for this evidence step.
The repair

A small line with a large consequence.

no validation that outputFunctionName / localsName / destructuredLocals are valid JS identifiers before they are concatenated into compiled code.

Before
if (opts.outputFunctionName) {
  prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
After the patch
// validate each option name against /^[a-zA-Z_$][0-9a-zA-Z_$]*$/ and throw otherwise (fixed in 3.1.7, commit 15ee6985).
Sources & further reading

The original record.

Read the historical advisory and vulnerability record behind this reconstruction.

Open the Lachesis quickstart