CWE-94, Improper Control of Generation of Code (Code Injection), is the weakness behind a lot of the worst remote code execution bugs. The product builds all or part of a code segment from externally influenced input, and does not neutralize the special elements that could change the syntax or behavior of that code.
MITRE lists interpreted languages and AI/ML systems as the platforms. If your application assembles code as a string and then runs it, this is yours. That includes template engines, expression evaluators, plugin systems, and, increasingly, apps that ask a language model to write code and then execute the answer.
How it happens
The pattern is always the same. Trusted code text and untrusted data get joined, then handed to something that executes the result.
# Vulnerable: user text becomes part of a Python expression
def calculate(user_expression):
return eval("1 + " + user_expression)
# user_expression = "__import__('os').system('id')"
The developer expected a number. The attacker supplied a function call.
The observed examples on MITRE's page show the same flaw in newer settings:
- CVE-2023-29374: a math component in an LLM framework translates user input into a Python expression that is passed to
exec(), allowing code execution. MITRE calls it one variant of a prompt injection attack. - CVE-2024-5565: a Python based library puts user input in an LLM prompt to generate code, then feeds that code to
exec(). - CVE-2024-4181: a framework for LLM applications allows eval injection through a crafted response from a hosting provider.
- CVE-2022-2054: a Python compiler uses
eval()to execute malicious strings as Python code.
Notice what changed in the first three. The "input" is no longer a form field. It is model output, and a model's output is only as trustworthy as the text that shaped it.
What an attacker gains
MITRE lists four consequences, and together they describe a full compromise:
- Execute unauthorized code or commands. The attacker alters the intended control flow, with impact on confidentiality, integrity and availability.
- Bypass protection mechanisms. Injectable code sometimes controls authentication, which can make the flaw remotely exploitable.
- Gain privileges. Injected code can reach resources the attacker is directly prevented from touching.
- Hide activities. The actions of injected code are often not logged.
How to find it
MITRE says static analysis (SAST) can find some instances by modeling data flow into code generation sites. Start with a grep for the usual suspects: eval, exec, compile, dynamic import, template rendering from user supplied template strings, and any call that executes a string.
Then ask one question for each hit: can any part of this string trace back to a request, a file, a database field, a message queue, or a model response? If yes, treat it as a finding until proven otherwise.
Dependencies count too. Two of the CVEs above are in frameworks, not in application code. A free in-browser dependency scan at gold.safeguard.sh/scan takes a manifest, keeps the file on your machine, and checks up to 100 dependencies against known vulnerabilities, so you can see whether a library you ship has this class of bug. Reachability analysis then tells you whether your code actually calls the vulnerable function.
Click through it at your own pace, or open it full screen.
How to fix it
MITRE's mitigations, in the order that tends to pay off:
- Refactor so you do not generate code dynamically. This is the real fix. Most
evalcalls exist because someone wanted a shortcut, not because they needed one. - Validate input with an allowlist. Assume all input is malicious and accept only known good values. MITRE adds a warning worth repeating: checking that input is alphanumeric may be insufficient if you are building a function call, because an attacker might still reference dangerous function names.
- Run the code in a sandbox. A jail such as chroot or AppArmor limits what executed code can reach. MITRE notes it limits impact on the operating system, but the rest of your application can still be compromised.
Here is the fixed calculator. It never executes text.
import operator
OPS = {"+": operator.add, "-": operator.sub, "*": operator.mul}
def calculate(a, op, b):
if op not in OPS:
raise ValueError("unsupported operator")
return OPS[op](float(a), float(b))
The input is split into typed parts, the operator is looked up in a fixed table, and numbers are parsed as numbers. There is nothing for an attacker to inject into.
For LLM driven features, apply the same rule. Do not execute model generated code in your application's own process. If you must run it, do so in a separate sandbox with no credentials and no network.
Quick check
Why is "only allow letters and numbers" sometimes not enough?
When code invokes a function by name, an alphanumeric string can still name a dangerous function. MITRE says to use stringent allowlists that limit which constructs are allowed.
Is model output a trusted source for code to execute?
No. Several of the observed CVEs involve model output reaching exec() or eval(). Treat that output like any other untrusted input.