템플릿 인젝션

C# 템플릿 인젝션

설명

서버 측 템플릿 인젝션(SSTI)은 신뢰할 수 없는 입력이 데이터가 아니라 실행 가능한 템플릿 소스로 해석될 때 발생합니다. Razor 템플릿은 서버 측 C# 지시문과 식을 포함할 수 있으므로, 공격자가 템플릿 본문을 제어하면 템플릿 엔진에 노출된 객체와 애플리케이션 프로세스 권한을 악용할 수 있습니다.

템플릿 소스, 템플릿 키, 모델 값은 서로 다릅니다.

  • 템플릿 소스: 엔진이 컴파일하거나 해석하는 본문입니다. 신뢰할 수 없는 값을 여기에 넣으면 취약합니다.
  • 템플릿 키: 이미 등록된 템플릿을 찾는 식별자입니다. 키를 고정된 신뢰 템플릿에 매핑하면 사용자가 템플릿 코드를 작성하지 못합니다.
  • 모델 값: 템플릿에 표시할 데이터입니다. 신뢰할 수 없는 값은 템플릿 본문에 이어 붙이지 말고 모델로 전달해야 합니다.

잠재적 영향

  • 서버 측 비밀 및 애플리케이션 데이터 노출
  • 파일 시스템, 네트워크 또는 애플리케이션 기능 호출
  • CPU 및 메모리 고갈을 통한 서비스 거부
  • 템플릿 엔진과 실행 환경에 따라 임의 코드 실행

해결 방법

  1. Razor 템플릿 본문은 소스 코드, 배포 파일 또는 관리자가 통제하는 저장소에서만 가져옵니다.
  2. 사용자가 템플릿을 선택해야 한다면 입력 식별자를 고정된 템플릿 목록에 매핑합니다. 신뢰할 수 없는 컬렉션이나 실행 중 변경 가능한 목록을 허용 목록으로 사용하지 않습니다.
  3. 사용자 데이터는 템플릿 문자열에 결합하지 말고 모델 값으로 전달합니다. 이 조치는 HTML 출력 컨텍스트에 필요한 인코딩을 대체하지 않습니다.
  4. 문자 제거, 이스케이프 또는 이름만 sanitize인 도우미로 Razor 소스를 안전하게 만들 수 있다고 가정하지 않습니다. 예외적으로 입력 가능한 템플릿을 제한해야 한다면, 완전한 템플릿 단위의 고정 허용 목록처럼 검증 결과가 명확한 방법을 사용합니다.
  5. 레거시 RazorEngine의 프로세스 내 Code Access Security(CAS) 또는 AppDomain 격리를 보안 경계로 신뢰하지 않습니다. .NET 5 이상에서는 대부분의 CAS API가 폐기되었고 일부 호출은 아무 동작도 하지 않을 수 있습니다.

제품 요구사항상 사용자가 템플릿을 작성해야 한다면 Razor처럼 C#을 실행하는 엔진 대신 유지 관리되는 제한형 템플릿 엔진을 검토하세요. 예를 들어 Scriban을 사용하는 경우 알려진 실행 제한 우회가 수정된 7.2.6 이상을 사용하고, 새 TemplateContext에 명시적인 ScriptObject/ScriptArray 데이터와 필요한 함수만 노출하며, 반복·재귀·출력 크기·정규식 시간·취소 제한을 설정해야 합니다. 풍부한 .NET 객체나 파일을 읽는 ITemplateLoader를 그대로 노출하지 마세요. Scriban의 안전 런타임도 프로세스 수준 샌드박스가 아니므로 강한 경계가 필요하면 별도 최소 권한 프로세스나 컨테이너에서 실행하세요.

예시

변경 전

csharp
using Microsoft.AspNetCore.Mvc;
using RazorLight;

async Task<string> Render(
    [FromBody] string template,
    RazorLightEngine engine)
{
    return await engine.CompileRenderStringAsync(
        "user-template",
        template, // 신뢰할 수 없는 입력이 Razor 소스로 컴파일되고 렌더링됨
        new { Name = "Ada" });
}

변경 후

csharp
using Microsoft.AspNetCore.Mvc;
using RazorLight;

async Task<string> Render(
    [FromBody] string name,
    RazorLightEngine engine)
{
    const string trustedTemplate = "Hello @Model.Name";
    return await engine.CompileRenderStringAsync(
        "welcome",
        trustedTemplate,
        new { Name = name }); // 입력은 템플릿 소스가 아니라 모델 값
}

설명:

  • 변경 전: CompileRenderStringAsync의 두 번째 인수는 템플릿 content입니다. 요청 본문을 이 위치에 전달하면 공격자가 Razor 소스를 작성할 수 있습니다.
  • 변경 후: 템플릿 본문은 배포자가 관리하는 상수이고, 요청 데이터는 모델의 Name 값으로만 전달됩니다.

참조