설명
System.Linq.Dynamic.Core는 문자열을 파싱해 LINQ 표현식 트리를 만듭니다. 요청에서 받은 문자열 전체를 Where, OrderBy, Select 또는 DynamicExpressionParser.ParseLambda의 표현식 인수로 전달하면 공격자가 데이터 값뿐 아니라 조건, 정렬, 투영과 같은 쿼리 구조를 바꿀 수 있습니다.
고정된 표현식의 @0, @1 자리표시자에 값을 전달하거나 라이브러리의 WhereInterpolated를 사용하는 것은 값과 표현식 구조를 분리합니다. 반대로 사용자 입력을 문자열 연결 또는 일반 보간 문자열로 표현식에 삽입하거나, 완성된 표현식 자체를 요청으로 받는 것은 파라미터화가 아닙니다.
이 문서의 버전별 참조는 System.Linq.Dynamic.Core 1.7.3을 기준으로 합니다. 1.3.0과 1.6.0에는 호출 가능한 메서드와 형식, object 메서드, OrderBy 표현식을 제한하는 보안 강화가 포함되었습니다. 이러한 제한은 과거의 코드 실행 및 리플렉션 접근 위험을 줄이지만, 공격자가 애플리케이션의 필터나 투영 정책을 선택하도록 허용해도 안전하다는 뜻은 아닙니다.
잠재적 영향
- 의도한 필터나 테넌트 경계를 우회해 허가되지 않은 레코드를 조회할 수 있습니다.
- 허용하지 않은 속성을 투영하거나 정렬 조건을 바꿔 민감한 데이터를 노출할 수 있습니다.
- 복잡하거나 비용이 큰 표현식으로 데이터베이스 또는 애플리케이션 자원을 과도하게 사용할 수 있습니다.
- 1.3.0 미만의 취약 버전에서는 CVE-2023-32571에 따른 임의 코드 및 명령 실행으로 이어질 수 있습니다. 1.6.0 미만에서는 CVE-2024-51417과 관련된 리플렉션 형식 및 정적 멤버 접근 위험이 있습니다.
해결 방법
- 가능하면 문자열 기반 Dynamic LINQ 대신 강타입 LINQ를 사용합니다.
- 클라이언트가 보내는 짧은 식별자를 서버가 소유한
Expression<Func<T, bool>>또는 고정 문자열에 매핑합니다. 입력값 전체를 표현식으로 취급하지 않습니다. - 값만 동적으로 전달해야 한다면 표현식은 고정하고
@0,@1자리표시자 또는WhereInterpolated를 사용합니다. 자리표시자는 속성명, 연산자, 정렬 방향 같은 구조를 파라미터화하지 않으므로 이러한 항목은 별도의 고정 맵으로 선택합니다. System.Linq.Dynamic.Core를 유지보수되는 최신 버전으로 업데이트합니다.AllowNewToEvaluateAnyType = false,AllowEqualsAndToStringMethodsOnObject = false,RestrictOrderByToPropertyOrField = true등 제한적인ParsingConfig기본값을 완화하지 말고, 사용자 지정 형식 공급자에는 필요한 형식만 노출합니다.- 최종 데이터 접근 권한은 표현식과 별도로 적용하고, 결과 수·쿼리 시간·복잡도에 제한을 둡니다.
예시
변경 전
using Microsoft.AspNetCore.Mvc;
using System.Linq;
using System.Linq.Dynamic.Core;
public sealed class User
{
public bool IsActive { get; init; }
public int Age { get; init; }
}
public sealed class SearchController
{
public IQueryable<User> Search(
[FromQuery] string predicate,
IQueryable<User> users)
{
return users.Where(predicate);
}
}
변경 후
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
public sealed class User
{
public bool IsActive { get; init; }
public int Age { get; init; }
}
public sealed class SearchService
{
private static readonly IReadOnlyDictionary<string, Expression<Func<User, bool>>> Filters =
new Dictionary<string, Expression<Func<User, bool>>>(StringComparer.Ordinal)
{
["active"] = user => user.IsActive,
["adult"] = user => user.Age >= 18
};
public IQueryable<User> Search(string filter, IQueryable<User> users)
{
if (!Filters.TryGetValue(filter, out var predicate))
{
throw new ArgumentException("Unsupported filter.", nameof(filter));
}
return users.Where(predicate);
}
}
설명:
- 변경 전: 요청의
predicate가 Dynamic LINQ 문법으로 그대로 파싱되므로 호출자가 조건 구조를 결정합니다. - 변경 후: 외부 입력은 고정된 키로만 사용되고, 실제 조건은 서버가 작성한 강타입 표현식에서 선택됩니다.
Dynamic LINQ를 유지해야 하고 값만 외부에서 받는 경우에는 users.Where("Age >= @0", minimumAge)처럼 표현식 구조를 고정합니다. minimumAge가 문자열이더라도 표현식 문법으로 다시 파싱되지 않고 @0 값으로 바인딩됩니다.
참조
- CWE-917: Expression Language Injection
- OWASP Top 10:2025 A05 Injection
- OWASP ASVS 5.0
- System.Linq.Dynamic.Core 1.7.3
- System.Linq.Dynamic.Core README 및 보안 관련 변경 사항
- System.Linq.Dynamic.Core 변경 기록
- Dynamic LINQ 구성 옵션
- Dynamic LINQ 쿼리 연산자
- GHSA-w65q-jcmv-28gj / CVE-2023-32571
- CVE-2024-51417
- KISA 소프트웨어 보안약점 진단가이드 2021