Assembly path injection

Loading C# assemblies from untrusted paths

Description

If external input selects an assembly file, the application can load managed code chosen or placed by an attacker with its own privileges. Assembly.LoadFrom, Assembly.LoadFile, Assembly.UnsafeLoadFrom and modern .NET's AssemblyLoadContext.LoadFromAssemblyPath do not turn a file into trusted code.

Path.GetFullPath, Path.GetFileName and .dll extension checks do not authenticate a file's origin or publisher. AssemblyLoadContext separates assembly versions and dependencies; it is not a security sandbox. Microsoft likewise warns that untrusted code cannot be loaded safely into a trusted .NET process.

This can overlap with path traversal, but the boundary differs: traversal concerns files outside the intended directory; assembly path injection lets input select the code to load, activate or execute.

Potential impact

  • Arbitrary managed code can run with the application's account permissions.
  • Files, credentials, databases and internal networks accessible to the application may be exposed.
  • Malicious plugins and dependencies can support persistence or further attacks inside the process.

Remediation

  1. Accept only a small plugin identifier, not a file path or assembly name.
  2. Map it on the server to reviewed fixed filenames or absolute paths. Restrict deployment permissions so untrusted users cannot modify the plugin directory or manifest.
  3. On modern .NET, load only approved paths through AssemblyLoadContext. If plugins need separate dependency resolution, configure AssemblyDependencyResolver with an approved plugin path rather than user input.
  4. For third-party plugins, verify publisher signatures or content hashes from a signed manifest under an explicit trust policy. Verify dependencies too. A hash supplied by an attacker alongside the file does not establish trust.
  5. A strong name identifies an assembly; it does not establish code trust. .NET Core and .NET 5 or later do not validate strong-name signatures at runtime. Use platform-appropriate Authenticode or equivalent signing policy when publisher authentication is required.
  6. If untrusted plugins must run, isolate them with least privilege in a separate operating-system process, container or virtual machine instead of loading them into the trusted process.

Avoid Assembly.UnsafeLoadFrom and legacy path-based AppDomain/Activator APIs in new code. Replacing them with modern APIs alone is insufficient: first restrict target selection and establish trust.

Examples

Before

csharp
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

[ApiController]
[Route("plugins")]
public sealed class PluginController : ControllerBase
{
    [HttpPost("load")]
    public Assembly Load([FromQuery] string assemblyPath)
    {
        // Normalizing to an absolute path does not establish file trust.
        string normalized = Path.GetFullPath(assemblyPath);
        return AssemblyLoadContext.Default.LoadFromAssemblyPath(normalized);
    }
}

After

csharp
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;

static class ApprovedPlugins
{
    // Only the deployment account may write to this directory.
    private const string PluginRoot = "/opt/example/plugins";

    public static Assembly Load(string pluginId)
    {
        string fileName = pluginId switch
        {
            "reporting" => "ReportingPlugin.dll",
            "billing" => "BillingPlugin.dll",
            _ => throw new ArgumentOutOfRangeException(nameof(pluginId))
        };

        string trustedPath = Path.GetFullPath(Path.Combine(PluginRoot, fileName));
        return AssemblyLoadContext.Default.LoadFromAssemblyPath(trustedPath);
    }
}

[ApiController]
[Route("plugins")]
public sealed class PluginController : ControllerBase
{
    [HttpPost("load")]
    public IActionResult Load([FromQuery] string pluginId)
    {
        Assembly plugin = ApprovedPlugins.Load(pluginId);
        return Ok(plugin.GetName().Name);
    }
}

The first example normalizes a user-selected path without verifying the assembly's origin or permissions. The second maps a limited identifier to a fixed filename. It depends on only trusted deployment identities being able to modify the directory and its artifacts.

Stop loading if publisher or content validation fails. Protect verification policies, trusted key stores and permission to use the plugins separately.

References