Code Injection
Code injection is a vulnerability where untrusted input reaches an interpreter or code generator, letting an attacker add code that the program then runs.
Also known as CWE-94, improper control of generation of code, code injection attack
Code injection is a vulnerability where an application builds part of a program from untrusted input and then runs it, turning an attacker's data into executing code in the app's own language. Tracked as CWE-94, it ranks #10 on the 2025 CWE Top 25 and underlies eval bugs, template injection, and CI pipeline attacks.
The most useful way to see code injection is as a boundary failure. Every interpreter is built to keep two things apart: the program, which is code, and the values it operates on, which are data. Code injection is what happens when input crosses from the data channel into the code channel. We think that framing matters because it predicts the fix: repair the boundary and the whole bug class closes, patch the symptom and it returns under a new name.
What is code injection?
#Code injection is a vulnerability in which untrusted input is incorporated into a code segment that an interpreter or code generator then executes. MITRE defines CWE-94 as constructing a code segment using externally influenced input without neutralizing "special elements that could modify the syntax or behavior of the intended code segment." In plain terms: the input stops being a value and becomes part of the instructions.
An interpreter normally separates instructions from data by construction. A SQL engine receives a query plan and, separately, the values to bind into it. A shell receives a command and its arguments. Code injection erases that separation by assembling the program text at runtime from a string that contains input, so the parser has no way to tell the developer's intent from the attacker's addition.
CWE-94 sits under CWE-74, improper neutralization of output, and CWE-913, improper control of dynamically managed code. Its children name the concrete sinks: CWE-95 for eval, CWE-96 for code saved and later run, and CWE-1336 for template engines. The 2025 CWE Top 25 places code injection at #10 with a score of 7.57 and seven entries already in CISA's Known Exploited Vulnerabilities catalog.
The figure contrasts the two designs. On the left, one channel: the developer writes render( "Hi " + name ), the untrusted value name is concatenated into that string, and the interpreter parses the result as one program, so an attacker-supplied call such as user.exit() executes alongside the greeting. On the right, two channels: the code template render( "Hi " + $1 ) is sent once and fixed, the value is sent separately as $1, and the same attacker string is treated as one inert value that is printed, never executed. The defense is structural, not a filter on the input.
Code injection versus command injection
#Code injection and command injection are distinct weaknesses that are routinely conflated. Code injection runs code in the target language, where, as OWASP puts it, the attacker is "only limited by the functionality of the injected language itself." Command injection instead runs operating-system commands through a shell, using code that already exists. SQL injection, cross-site scripting, LDAP injection, and file inclusion are all injection types that share the one mechanism, input crossing into code, and differ only in which interpreter receives it.
| Injection type | Interpreter it targets | CWE | 2025 CWE Top 25 |
|---|---|---|---|
| Code injection | A language evaluator or compiler (eval, Function, a template) | CWE-94 | #10 |
| OS command injection | The system shell | CWE-78 | #9, with 20 KEV CVEs, the most on the list |
| Command injection (general) | Any command grammar | CWE-77 | #23, the list's largest drop, down 10 |
| SQL injection | The database query engine | CWE-89 | #2 |
| Cross-site scripting | The browser's HTML and JavaScript parser | CWE-79 | #1 |
| Deserialization of untrusted data | The object-graph loader | CWE-502 | #15 |
The verdict is a single test. When the input decides what the program is, it is code injection; when it only decides which command the shell runs, it is command injection. The distinction guides triage and naming, but not the fix, which is identical for both: keep the input in the data channel so no interpreter ever reads it as code. File inclusion is the same mechanism seen once more, where a request-controlled path passed to a dynamic include turns an attacker's file into the program's next instructions.
How does code injection reach a modern application?
#Code injection reaches a modern application through any sink that turns input into code: a dynamic evaluator, a template compiler, a deserializer, or a dynamic include. Each is a place where a developer asked the runtime to build code from a value, usually for a legitimate reason, and then let untrusted input reach it.
Dynamic evaluation is the most direct sink. Python's own documentation says of eval: "Calling it with untrusted user-supplied input will lead to security vulnerabilities." Isolation does not rescue it. Node's documentation states that the node:vm module "is not a security mechanism" and must not run untrusted code, and vm2, a library built specifically to be that sandbox, shipped repeated escapes, including a critical one in 2026 exploitable across 66 releases. Restricting the environment is damage control, not prevention.
Template engines are code generators, which makes their inputs a sink too. The next-mdx-remote vulnerability, CVE-2026-0969, carried a CVSS score of 8.8: untrusted MDX compiled on the server could reach remote code execution, and the fix disabled JavaScript expressions by default. Deserializers are a quieter sink. Python's documentation warns that the pickle module "is not secure" and that malicious data "will execute arbitrary code during unpickling."
| Sink | CWE | A real instance | The structural fix |
|---|---|---|---|
| eval / Function / exec | CWE-95 | LangChain LLMMathChain (CVE-2023-29374) | Parse with a literal-only evaluator; never eval input |
| Template compiler | CWE-1336 | next-mdx-remote (CVE-2026-0969) | Disable code expressions; keep input out of template source |
| Deserializer | CWE-502 | Python pickle, Java readObject | Use a data-only format such as JSON for untrusted input |
| Dynamic include | CWE-98 into CWE-94 | A request-controlled include() path | Allowlist the fixed set of includable files |
Code injection in the software supply chain
#Code injection became a supply chain problem once attackers noticed the build pipeline is itself an interpreter. GitHub Actions expands ${{ }} expressions by textual substitution before the shell runs the script, so an attacker-controlled pull request title or branch name is spliced directly into shell code. GitHub's CodeQL classifies this expression injection as CWE-94.
The pattern has driven real releases of malware. In the Nx s1ngularity compromise of August 2025, a workflow step echoed the pull request title into a shell command, a title shaped like $(...) ran as bash and leaked the npm publishing token, and malicious packages were live for roughly four hours before removal. In the Ultralytics attack of December 2024, a crafted branch name injected into a format workflow poisoned the build cache, and the release pipeline shipped a crypto miner to PyPI as versions 8.3.41 and 8.3.42. In both, the reviewed source was clean; the injection lived in a field no diff showed.
# Vulnerable: the title is substituted into the script before bash parses it
- run: echo "Validating PR title: ${{ github.event.pull_request.title }}"
# Fixed: pass it through the environment, where it stays a value
- env:
TITLE: ${{ github.event.pull_request.title }}
run: echo "Validating PR title: $TITLE"The same boundary governs what the pipeline installs. A dependency that calls eval or runs an install script is code the pipeline executes, which is why install time, not deploy time, is where most supply chain attacks run. Socket's Uses eval alert flags any package that performs dynamic code execution, on the principle that what matters is the code actually in the package, not what its metadata claims. A malicious package that reaches remote code execution on a build runner needs no exploit once it is installed; it was handed the code channel by default. Provenance helps attribute an artifact but does not close this path, since a compromised maintainer publishes injectable code through a legitimate, signed pipeline.
How do you prevent code injection vulnerabilities?
#You prevent code injection by never building code out of input. Keep every untrusted value in a data channel the interpreter cannot execute, and the sinks above have nothing to act on.
- Remove the sink where you can. Replace
evalwith a parser that handles only the literals you expect, use parameterized APIs instead of string-built queries, and feed template engines through their value slots, never by assembling the template source from input. - Where dynamic execution is unavoidable, treat isolation as containment, not a fix. Overriding the available names is not a sandbox; Python's documentation notes that even with
__builtins__overridden "the executed code can still access all builtins." - In CI, pass untrusted context through
env:variables and reference the shell variable, rather than interpolating${{ }}into arun:block. From December 2, 2026, GitHub blocks pull_request_target workflows by default on public repositories, closing the most common venue for this class.
Each control also has an edge it does not cover, and a single library shows how narrow a fix can be. lodash's _.template compiles a template into a JavaScript function, so whatever reaches that generated function's source is a code-injection sink. CVE-2021-23337, fixed in 4.17.21 in February 2021, added validation for the variable option. Five years later, CVE-2026-4800, CVSS 8.1 and fixed in 4.18.0, showed the same Function() sink was still reachable through the imports object's key names, which the 2021 fix never validated, and reachable via prototype pollution because the template merged inherited keys.
// _.template is a code generator: this is the function it compiles and runs
const compiled = _.template("Hi <%= name %>");
console.log(compiled.source);
// => function(obj) { ... with (obj) { ... } ... return __p }
//
// A crafted `variable` (pre-4.17.21) or `imports` key (pre-4.18.0) adds an
// executable default-parameter expression to that function's signature.
// Each field was fixed only at its own version; the sink is the same Function().We verified both paths against the released versions: 4.17.20 accepts the variable injection, 4.17.21 blocks it but still accepts the imports key injection, and 4.18.0 is the first release to reject both. The lesson generalizes: "we patched the injection" usually means "we patched the one field we knew about," and the CWE-94 sink outlives the patch whenever another route to it remains.
Why code injection outlives its fixes
#Every fix for code injection removes one route into the code channel, yet the weakness keeps resurfacing because new interpreters keep appearing. A CI expression language, a template compiler's option object, an LLM tool-calling layer: each is a fresh place where input can become code, and each gets rediscovered by someone who assumed string building was safe this time. Prompt injection is the current frontier, where the code generator is a model and the untrusted input is a sentence. The durable defense has not changed since the first eval bug: treat every point where input could become code as the same problem, and keep the two channels apart by construction rather than by a list of strings to reject.
