로그 위조

C# Microsoft.Extensions.Logging 로그 위조(CWE-117)

설명

로그 위조(log forging 또는 log injection)는 신뢰할 수 없는 데이터가 로그 형식에서 특별한 의미를 갖는 문자와 함께 기록될 때 발생합니다. 줄 단위 텍스트 로그에서는 공격자가 CR, LF 또는 다른 줄 구분자를 삽입해 별도의 이벤트처럼 보이는 행을 만들 수 있습니다. 구분자로 나뉜 로그, 터미널, HTML 로그 뷰어 또는 자동 파서에서는 해당 형식의 구분자나 제어 문자가 레코드 구조와 표시 내용을 바꿀 수 있습니다.

Microsoft.Extensions.Logging의 메시지 템플릿과 이름이 지정된 속성은 로그 구조와 검색 가능성을 유지하는 데 유용하지만, 모든 로깅 공급자가 속성 값의 줄바꿈을 자동으로 무력화한다는 뜻은 아닙니다. 최종 공급자, 전송 형식, 수집기와 뷰어가 같은 출력 계약을 지키는지 확인해야 합니다.

잠재적 영향

  • 존재하지 않는 성공·실패 이벤트를 삽입하거나 실제 공격 흔적을 숨길 수 있습니다.
  • 감사 로그의 무결성과 부인 방지 특성이 훼손되어 사고 조사와 경보 규칙이 잘못된 결론을 낼 수 있습니다.
  • 후속 파서의 레코드나 필드 구조를 손상시켜 탐지 누락, 처리 실패 또는 2차 인젝션을 일으킬 수 있습니다.
  • 매우 긴 입력을 그대로 기록하면 로그 저장 공간과 수집 파이프라인을 소진할 수 있습니다.

해결 방법

출력 형식에서 인코딩

가능하면 로깅 공급자가 구조화된 형식을 직접 생성하고 각 값을 해당 형식에 맞게 인코딩하도록 구성합니다. 예를 들어 .NET의 JSON 콘솔 포매터는 Utf8JsonWriter로 JSON 문자열 값을 작성합니다. 다만 AddJsonConsole을 사용했다는 사실만 가정하지 말고, 프로덕션에서 활성화된 모든 공급자와 수집·변환 단계를 확인하고 실제 출력에 공격 문자열을 넣어 테스트해야 합니다.

메시지 템플릿은 상수로 유지하고 신뢰할 수 없는 값은 이름이 지정된 속성으로 전달합니다. 이는 구조화된 로깅과 CA2254 준수에 필요하지만, 줄 단위 텍스트 공급자에서 CR/LF를 무력화하는 방어를 대신하지는 않습니다.

줄 단위 텍스트 로그에서 줄 구분자 무력화

.NET 6 이상에서는 String.ReplaceLineEndings(" ")를 사용해 인식되는 모든 줄바꿈 시퀀스를 줄바꿈이 아닌 값으로 바꿀 수 있습니다. 이 메서드는 CR, LF, CRLF뿐 아니라 NEL, LS, FF, PS도 처리합니다. 인자 없는 ReplaceLineEndings()는 줄바꿈을 현재 플랫폼의 줄바꿈으로 정규화할 뿐 제거하지 않으므로 보안 경계로 사용하면 안 됩니다.

줄 구분자 교체는 줄 단위 텍스트 로그의 레코드 분리를 막기 위한 것입니다. CSV 같은 구분자 기반 형식, 터미널 제어 시퀀스 또는 HTML 뷰어에는 각각의 최종 출력 컨텍스트에 맞는 인코딩이 추가로 필요합니다. 다른 출력용 인코더의 이름이나 Sanitize 같은 헬퍼 이름만으로 로그에 안전하다고 판단하지 마세요.

값의 허용 범위가 LOGIN, LOGOUT처럼 닫혀 있다면 변경되지 않는 리터럴 허용 목록으로 검증할 수 있습니다. 외부 설정이나 사용자 값이 섞인 목록, 검증 후 변경된 값, 런타임에 수정되는 컬렉션은 같은 보장을 제공하지 않습니다. 로그에 기록하는 입력 길이도 제한하고 비밀번호, 토큰, 암호화 키와 같은 비밀은 기록하지 마세요.

예시

변경 전

csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("audit")]
public sealed class VulnerableAuditController : ControllerBase
{
    private readonly ILogger<VulnerableAuditController> _logger;

    public VulnerableAuditController(ILogger<VulnerableAuditController> logger)
    {
        _logger = logger;
    }

    [HttpPost]
    public IActionResult Login([FromQuery] string user)
    {
        _logger.LogWarning("Failed login for {User}", user);
        return Ok();
    }
}

위 코드는 user가 텍스트 로그의 새 레코드를 만들 수 있는 줄 구분자를 포함해도 그대로 전달합니다.

변경 후

csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

[ApiController]
[Route("audit")]
public sealed class SafeAuditController : ControllerBase
{
    private readonly ILogger<SafeAuditController> _logger;

    public SafeAuditController(ILogger<SafeAuditController> logger)
    {
        _logger = logger;
    }

    [HttpPost]
    public IActionResult Login([FromQuery] string user)
    {
        var safeUser = user.ReplaceLineEndings(" ");
        _logger.LogWarning("Failed login for {User}", safeUser);
        return Ok();
    }
}

이 예시는 줄 구분자를 공백으로 바꿉니다. 실제 로그 공급자와 후속 출력 형식에 필요한 인코딩은 별도로 확인하세요.

참조