Skip to content

CWE-94: Improper Control of Generation of Code ('Code Injection')

Overview

Code injection occurs when an application dynamically generates and executes code using untrusted input, allowing attackers to inject and execute arbitrary code within the application's runtime environment. Unlike command injection (which executes OS commands), code injection executes in the application's programming language context with full access to application internals, variables, functions, database connections, file system, and secrets.

Common vulnerable patterns include eval(), exec(), Function(), and dynamic template rendering in unsafe mode across languages like Python, JavaScript, PHP, Ruby, Java, and C#.

Relationship to Other CWEs

  • CWE-94 (this page) - general code injection via dynamic code generation.
  • CWE-95 (Eval Injection) - the narrower child, specific to unsafe use of eval().

Two neighbours account for most of the mistriage in this area, because they also end in arbitrary code running and are often found in the same file:

  • CWE-502 (Deserialization of Untrusted Data) - Python pickle, unsafe yaml.load(), PHP unserialize(), .NET BinaryFormatter, Java ObjectInputStream. Code executes, but the sink is a deserializer handed a serialized object graph rather than an evaluator handed source text, and the fix is a different one - move to a data-only format or a restricted loader.
  • CWE-134 (Externally-Controlled Format String) - a user-supplied format string. In C this reaches memory corruption; in Python a user-controlled str.format() template walks attributes and reads module globals. That is information disclosure, not code execution - format fields cannot call - so filing it as CWE-94 overstates it.

The question that separates them: is the attacker supplying source text to be evaluated (CWE-94), a serialized object (CWE-502), or a format string (CWE-134)?

OWASP Classification

A05:2025 - Injection

Risk

Critical: Code injection gives an attacker the same power as the application's own code, so one finding can compromise the whole application and the infrastructure behind it. Anything the process can reach is readable - databases, files, memory, application secrets - and writable too, so an attacker can rewrite session state to impersonate a user, or change business logic such as pricing, inventory, or access control. From the same position they can run system commands on the host, install a backdoor, pivot to internal systems and services, exhaust the application's resources, or crash it outright. Code injected into a path every request passes through reaches every user.

Common Vulnerable Patterns

The sinks by language:

  • Python: eval(user_input), exec(user_code), compile(), __import__()
  • JavaScript: eval(userCode), new Function(userCode)(), browser setTimeout(userCode, 1000), node:vm used as a security boundary
  • PHP: eval($_GET['code']), include/require of a user-controlled path. On PHP 7 and earlier also string-form assert() and create_function() - both withdrawn in PHP 8, so on a current target they are dead code rather than live sinks
  • Ruby: eval(params[:code]), instance_eval(), class_eval()
  • Java: GroovyShell.evaluate(), SpEL against a StandardEvaluationContext, BeanShell, ScriptEngine.eval(userInput) - the last resolves to null for JavaScript on JDK 15+ unless an engine was added back as a dependency
  • C#: Roslyn dynamic compilation (CSharpCompilation), expression evaluators with reflection or dangerous types enabled, CSharpCodeProvider.CompileAssemblyFromSource() on .NET Framework

Whatever the language, code that runs at this point has the same access as the application itself:

  • Application variables and functions
  • Database connections and queries
  • File system and network access
  • Cryptographic keys and secrets
  • User sessions and authentication state

Remediation Steps

Core Principle: Never execute dynamically generated code derived from untrusted input; remove eval/dynamic compilation or strictly sandbox with allowlists.

Trace the Data Path

Work out how untrusted data reaches dynamic code execution:

  • Source: where untrusted data enters - HTTP parameters, form inputs, file uploads, API requests, database fields populated by users, WebSocket messages
  • Sink: code execution functions such as eval(), exec(), Function(), compile(), ScriptEngine, and template rendering in unsafe mode. Beyond the obvious names, look for dynamic compilation, dynamic import or module loading, template engines compiling a user-supplied template body, and expression-language evaluation. Deserialization sinks reached by untrusted data belong to CWE-502 - worth fixing, but a different finding with a different fix
  • Data flow and missing controls: any sanitization between source and sink, such as string replacement, encoding, or filters, and whether it actually holds

Eliminate Dynamic Code Execution (Primary Defense)

The safest fix is to remove dynamic code execution entirely. The sinks fall into five categories, and every ecosystem has all of them under different names. The language pages below name the concrete APIs; what matters here is recognising the category:

  • Evaluating source text at runtime - the interpreter's own eval or exec entry point.
  • Compiling source text at runtime - a compiler or script engine invoked on a string.
  • Dynamic import or module loading - resolving a module name from input, which executes that module's top level.
  • Template rendering in an unsafe mode - a template engine that treats the template body as executable rather than as data.
  • Expression-language evaluation - a restricted-looking mini-language that can still reach arbitrary methods unless explicitly locked down.

The only safe amount of any of them with attacker-controlled input is zero.

Replace with Safe Alternatives

Replace code execution with declarative, safe approaches:

Configuration-driven logic:

  • JSON/YAML configuration files
  • Rule engines (Drools, Easy Rules)
  • Expression languages with restricted capabilities
  • Template engines rendering a trusted template with user-controlled data as values only

The boundary in that last one is not the one template engines are usually discussed in terms of. What makes a template engine a safe alternative here is that the template body comes from your source tree and only the values substituted into it come from the request. Auto-escaping is a different control answering a different question: it stops user data from being interpreted as markup by the browser, which addresses CWE-79, and it does nothing about a user who supplies the template itself. Jinja2 and Handlebars will both compile and execute an attacker-supplied template body with auto-escaping fully enabled. If the template source is user-controlled, you have a code-injection sink whatever the escaping settings say. The language pages cover what each engine offers and what none of them offer; Jinja2's SandboxedEnvironment, for example, needs 3.1.6 or later, because earlier releases had sandbox-escape bypasses such as CVE-2025-27516.

Safe dynamic dispatch:

  • Switch/case statements with pre-defined options
  • Strategy pattern (pre-defined implementations)
  • Command pattern with registered handlers
  • Plugin systems with sandboxing

Data transformation:

  • JSON path/jq for data queries
  • XPath/XQuery for XML (with restrictions)
  • SQL with parameters for database queries
  • pandas/NumPy for data manipulation

Business rules:

  • Decision tables
  • Workflow engines
  • State machines
  • Declarative policy files

Sandbox and Restrict (Only If Absolutely Required)

If code execution cannot be avoided, isolate it as far as the platform allows.

Python:

  • Use RestrictedPython (limited Python subset)
  • ast.parse() + allowlist AST node types
  • Run in Docker container with restricted capabilities

JavaScript:

  • Separate process or container with no ambient access to application secrets
  • Web Workers with restricted APIs for browser-side isolation
  • iframe with sandbox attribute for browser-side untrusted content
  • Do not rely on Node's vm module or discontinued sandbox packages as the security boundary for adversarial code

Java:

  • OSGi bundles with limited imports
  • GraalVM with restricted contexts
  • Separate JVM process with limited classpath

General sandboxing requirements:

  • Separate process with no network access
  • Extremely short timeout (prevent DoS)
  • Memory limits
  • No access to filesystem, network, or system calls
  • Allowlist of permitted functions/modules

Add Input Validation (Defense-in-Depth Only)

If sandboxing is used, add validation as additional protection:

  • Maximum length limits, so a pathological input cannot exhaust the parser
  • Character allowlist - the narrowest set the feature actually needs
  • AST parsing that allowlists permitted node types, rejecting everything else

Validation on its own is not a fix. Bypasses are common and new ones are found regularly, so treat it only as defense-in-depth alongside sandboxing.

Note what is deliberately absent: a denylist of keywords such as eval, exec, import or system, and regular expressions matching "code-looking" patterns. Both inspect the text for known-bad shapes, and both are defeated by concatenation, by alternate encodings, and by object-graph traversal that never spells the banned word - see Common Pitfalls. Where a character allowlist is narrow enough to make those words unrepresentable, the denylist is unreachable code; where it is loose enough to allow them, the denylist does not hold. Either way it adds no defence while making the validation look stronger than it is, which is the more expensive of the two failures.

Test the Fix

Verify your fixes with malicious inputs:

  • Test code injection: __import__('os').system('whoami'), eval('malicious')
  • Test object access: this.constructor.constructor('return process')().env
  • Test import injection: require('child_process').exec('ls')
  • Test encoding bypasses: \x5f\x5fimport\x5f\x5f, Base64-encoded payloads
  • Check that legitimate functionality still works
  • Re-scan with the security scanner to confirm the issue is resolved

Basic Code Injection

# Test executing arbitrary code

input: "__import__('os').system('whoami')"
input: "open('/etc/passwd').read()"
input: "globals()['__builtins__']['eval']('malicious')"
input: "exec('import socket; ...')"

# Expected: Not executed, or sandboxed and blocked

Object/Variable Access

// Test accessing application internals

input: "this.constructor.constructor('return process')().env"
input: "require('child_process').exec('whoami')"
input: "Object.keys(global)"

# Expected: Access denied or variables undefined in sandbox

Import/Require Injection

# Test loading dangerous modules

input: "__import__('subprocess').call(['ls', '-la'])"
input: "import('fs').then(fs => fs.readFileSync('/etc/passwd'))"
input: "System.loadLibrary('malicious')"

# Expected: Import blocked or module not available

Encoding Bypass

Test with encoded payloads:

input: "\x5f\x5fimport\x5f\x5f" (__import__)
input: "eval(atob('ZXZhbCgnYWxlcnQoMSknKQ=='))" (Base64)
input: "Function('return this')()" (access global)

Expected: Encoding doesn't bypass restrictions

Verify Removal

# Search codebase for dangerous functions

grep -r "eval(" --include="*.py" --include="*.js" --include="*.php"
grep -r "exec(" --include="*.py"
grep -r "new Function" --include="*.js"
grep -r "ScriptEngine" --include="*.java"

# Expected: No usage with user input, or all properly sandboxed

Common Pitfalls

  • Denylisting dangerous keywords or function names: Rejecting input that contains strings like eval, import, exec, or system is trivially bypassed with string concatenation, alternate encodings, or an equivalent API the denylist didn't anticipate. Inspecting the text of untrusted input for known-bad substrings, rather than restricting what the execution engine itself can do, is not a fix.
  • Restricted global namespace treated as a sandbox: Removing dangerous names from the globals/builtins available to an evaluator (for example deleting import or a module reference from scope) feels like sandboxing but usually is not - most dynamic languages expose object introspection paths (walking class hierarchies, constructors, or metaobjects) that reach the same dangerous functionality without referencing the removed name directly.
  • Swapping sinks instead of eliminating dynamic execution: Replacing one dangerous evaluator with a different-sounding one (a different scripting engine, or a general-purpose expression library) without checking whether it still allows type/class references, reflection, or method invocation - many expression languages support the same capability the original eval-style API did, just through different syntax.
  • Sandboxing without resource limits: Isolating code execution in a subprocess or container but omitting a timeout, memory cap, or network restriction still permits denial-of-service or secret exfiltration (if the sandbox has ambient credentials or network access), even though direct code execution on the host is prevented.

Language-Specific Guidance

For detailed implementation examples in specific programming languages:

  • C#/.NET - avoiding Roslyn/CSharpCodeProvider on untrusted input, dispatch tables, and what DynamicExpresso does and does not restrict by default
  • Java - GroovyShell and SpEL risks, JEXL sandboxing, SimpleEvaluationContext, and how to tell a live ScriptEngine finding from a dead one
  • PHP - eval(), allowlisted include/require, which legacy sinks PHP 8 has already closed, and why disable_functions cannot disable eval
  • Python - ast.literal_eval, a restricted AST evaluator, Jinja2 sandboxing, allowlisted plugin imports
  • JavaScript/Node.js - math.js, AST validation, out-of-process isolation, avoiding eval/Function, CSP headers

Additional Resources