Dynamic code injection in scripting API
ID |
csharp.code_injection |
Severity |
critical |
Resource |
Injection |
Language |
CSharp |
Tags |
CWE:95, NIST.SP.800-53, OWASP:2021:A3, PCI-DSS:6.5.1 |
Description
Improper neutralization of directives in dynamically evaluated code ('Eval Injection').
Code Injection vulnerabilities occur when an application dynamically executes code containing untrusted input from users.
Rationale
Code injection vulnerabilities in C# involving eval-like functionality occur when untrusted input is dynamically compiled and executed as code. This risk arises primarily from using methods that allow runtime code generation or execution without proper input validation.
The core issue stems from directly embedding user-controlled input into dynamically executed code. For example, concatenating unsanitized input into a C# code string that gets compiled and executed allows attackers to inject arbitrary code. This could lead to:
-
Remote code execution (e.g., deleting files, exfiltrating data)
-
Application logic manipulation (e.g., bypassing authentication)
-
Denial-of-service attacks (e.g., infinite loops).
Such vulnerabilities originate from trusting client-side input as benign and inadequate input validation, along with the overuse of dynamic code execution such as eval-like methods.
C# is a compiled language but there are ways to compile code at runtime. The traditional way was to use Reflection.Emit
but it has been replaced with the (now obsolete) CodeDom with CSharpCodeProvider
and VBCodeProvider
classes, or the newer Roslyn and Razor templates.
The following code using CodeDom was adapted from an existing application vulnerable to code injection:
using Microsoft.CSharp;
using System.CodeDom.Compiler;
// Injects user input as code in a C# function
// functionTemplate and classTemplate not shown
string function = functionTemplate.Replace("// #code#", userControlledInput);
string finalCode = classTemplate.Replace("// #function#", function);
var provider = new CSharpCodeProvider();
var parameters = new CompilerParameters { GenerateInMemory = true };
// VULNERABLE to code injection
CompilerResults results = provider.CompileAssemblyFromSource(parameters, finalCode);
// Invoke the compiled function using reflection
Type type = results.CompiledAssembly.GetType("MyClass");
MethodInfo method = type.GetMethod("MyFunction");
method.Invoke(null, null);
An attacker may pass Process.Start("calc.exe");
or take more dangerous actions, like downloading and executing malware or MetaExploit, or opening a reverse shell.
Using templating engines like Razor to run C# arbitrary expressions at runtime opens up the possibility of code injection:
using RazorEngine;
using RazorEngine.Templating;
// VULNERABLE to code injection
// untrustedInput is interpreted as part of the C# expression template
string template = $@"
Hello {untrustedInput}, welcome to RazorEngine!
Today is @Model.Today
";
var result = Engine.Razor.RunCompile(template, "templateKey", null,
new { Today = DateTime.Now.ToString() }
);
An offender can use @System.Diagnostics.Process.Start("powershell -e do_evil")
and get a reverse shell, for example.
Remediation
To mitigate Code Injection vulnerabilities, follow these best practices:
-
Avoid Dynamic Code Execution: Where possible, avoid using dynamic script execution or reflection with untrusted input.
-
Input Validation and Sanitization: Assume all input is potentially malicious. Rigorously validate all user inputs to confirm they adhere to expected formats, and sanitize them (a whitelisting approach is recommended) to remove potentially harmful content.
-
Canonicalization: Decode and canonicalize inputs to a standard internal representation before validation. This helps prevent bypassing input filters through encoding tricks
For the first example, it is difficult and error-prone to validate input as allowed C# code to be compiled and executed. So: Do not use runtime compilation with untrusted input ! Period.
If you must allow dynamic code execution, use a sandboxed scripting engine like Roslyn scripting API with security restrictions, ClearScript for JavaScript with V8, or other code interpreters in a sandboxed environment or in an isolated environment (e.g., Docker container or AppDomain with limited permissions).
For the vulnerable Razor template example, the following code should be used instead:
using RazorEngine;
using RazorEngine.Templating;
// FIXED, the value in the Name property is injected bt Razor as a literal string
string template = $@"
Hello @Model.Name, welcome to RazorEngine!
Today is @Model.Today
";
var result = Engine.Razor.RunCompile(template, "templateKey", null,
new { Name = untrustedInput, Today = DateTime.Now.ToString() }
);
Never concatenate unsanitized user input as part of the Razor template, use model binding instead. This is similar to parameterized queries against SQL injection. Please note that @Raw(Model.Name)
will prevent the input from being HTML-escaped, leading to potential cross-site scripting vulnerabilities.
Configuration
The detector has the following configurable parameters:
-
sources
, that indicates the source kinds to check. -
neutralizations
, that indicates the neutralization kinds to check.
Unless you need to change the default behavior, you typically do not need to configure this detector.
References
-
CWE-95 : Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection').
-
OWASP Top 10 2021 - A03 : Injection.
-
OWASP Code Injection explained.
-
Code Injection to RCE with .NET, from Stratum Security’s blog.