설명
외부 입력이 실행할 어셈블리 파일의 경로를 선택하면 애플리케이션이 공격자가 배치하거나 선택한 관리 코드를 자신의 권한으로 로드할 수 있습니다. Assembly.LoadFrom, Assembly.LoadFile, Assembly.UnsafeLoadFrom뿐 아니라 최신 .NET의 AssemblyLoadContext.LoadFromAssemblyPath도 전달받은 파일을 신뢰할 수 있는 코드로 바꾸어 주지 않습니다.
Path.GetFullPath, Path.GetFileName, .dll 확장자 검사와 같은 경로 변환은 파일의 출처나 게시자를 인증하지 않습니다. 또한 AssemblyLoadContext는 어셈블리 버전과 종속성 충돌을 분리하는 도구이지 보안 샌드박스가 아닙니다. Microsoft도 신뢰할 수 없는 코드를 신뢰된 .NET 프로세스 안에 안전하게 로드할 수 없다고 안내합니다.
이 취약점은 일반적인 경로 탐색과 겹칠 수 있지만 초점이 다릅니다. 경로 탐색은 의도한 디렉터리 밖의 파일 접근을 다루고, 어셈블리 경로 주입은 외부 입력이 로드·활성화·실행할 어셈블리 파일을 선택하여 실행 코드의 신뢰 경계를 넘는 상황을 다룹니다.
잠재적 영향
- 애플리케이션 계정 권한으로 임의의 관리 코드가 실행될 수 있습니다.
- 파일, 자격 증명, 데이터베이스, 내부 네트워크 등 애플리케이션이 접근할 수 있는 자원이 노출될 수 있습니다.
- 악성 플러그인과 그 종속성이 프로세스에 로드되어 지속성 확보나 추가 공격에 사용될 수 있습니다.
해결 방법
- 요청에서 파일 경로나 어셈블리 이름을 받지 말고, 작은 플러그인 식별자만 받습니다.
- 식별자를 검토된 고정 파일명 또는 절대 경로에 서버 측에서 매핑합니다. 플러그인 디렉터리와 매니페스트는 신뢰할 수 없는 사용자가 수정할 수 없도록 배포 권한을 제한합니다.
- 최신 .NET에서는 승인된 경로를
AssemblyLoadContext로 로드합니다. 플러그인별 종속성 분리가 필요하면 사용자 입력이 아닌 승인된 플러그인 경로로AssemblyDependencyResolver를 구성합니다. - 제3자 플러그인을 허용해야 한다면 조직의 명시적인 신뢰 정책에 따라 게시자 서명이나 서명된 매니페스트의 콘텐츠 해시를 검증하고 종속성도 함께 검증합니다. 공격자가 파일과 함께 제공한 해시를 비교하는 것만으로는 신뢰를 증명할 수 없습니다.
- strong name은 어셈블리 식별자일 뿐 코드 신뢰 검증 수단이 아닙니다. .NET Core와 .NET 5 이상에서는 런타임이 strong-name 서명을 검증하지 않습니다. 게시자 인증이 필요하면 플랫폼에 맞는 Authenticode 또는 동등한 코드 서명 정책을 사용합니다.
- 실제로 신뢰할 수 없는 플러그인을 실행해야 한다면 동일 프로세스에 로드하지 말고 OS 프로세스, 컨테이너 또는 가상화 경계에서 최소 권한으로 격리합니다.
새 코드에서는 Assembly.UnsafeLoadFrom과 레거시 경로 기반 AppDomain/Activator API를 사용하지 않습니다. 이러한 API를 최신 API로 바꾸는 것만으로는 안전해지지 않으며, 로드 대상의 선택과 신뢰를 먼저 제한해야 합니다.
예시
변경 전
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)
{
// 절대 경로로 정규화해도 파일의 신뢰성은 검증되지 않습니다.
string normalized = Path.GetFullPath(assemblyPath);
return AssemblyLoadContext.Default.LoadFromAssemblyPath(normalized);
}
}
변경 후
using Microsoft.AspNetCore.Mvc;
using System;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
static class ApprovedPlugins
{
// 이 디렉터리는 배포 계정만 쓸 수 있어야 합니다.
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);
}
}
설명:
- 변경 전: 요청 값이 실제 로드 경로를 결정합니다. 경로 정규화는 경로 형식만 바꾸며 어셈블리의 출처나 권한을 검증하지 않습니다.
- 변경 후: 요청 값은 제한된 식별자로만 사용되고, 서버가 각 식별자를 고정 파일명에 매핑합니다. 예시의 안전성은 플러그인 디렉터리와 배포 산출물이 신뢰할 수 있는 주체만 수정할 수 있다는 조건에 의존합니다.
게시자나 콘텐츠 검증에 실패하면 로드를 중단하세요. 검증 정책과 신뢰 키 저장소, 플러그인 사용 권한도 별도로 보호해야 합니다.
참조
- CWE-829: Inclusion of Functionality from Untrusted Control Sphere
- OWASP Top 10:2025 A08 Software or Data Integrity Failures
- OWASP Top 10:2021 A08 Software and Data Integrity Failures
- .NET 플러그인 애플리케이션 만들기
- AssemblyLoadContext 이해
- AssemblyLoadContext.LoadFromAssemblyPath
- Assembly.UnsafeLoadFrom
- .NET strong-named assemblies