로그 정보 공개

OWASP 카테고리: MASVS-STORAGE: 저장소

개요

로그 정보 공개는 앱이 민감한 정보를 기기 로그에 출력하는 취약점 유형입니다. 악의적인 행위자에게 노출되는 경우 이러한 민감한 정보는 바로 악용될 수도 있고(예: 사용자 인증 정보 또는 개인 식별 정보(PII)), 추가적인 공격을 가능하게 할 수도 있습니다.

이 문제는 다음과 같은 시나리오에서 발생할 수 있습니다.

  • 앱에서 생성된 로그:
    • 이 로그를 통해 승인되지 않은 행위자에 의도적으로 액세스할 수 있지만, 민감한 정보가 실수로 포함되어 있습니다.
    • 로그에 의도적으로 민감한 정보가 포함되며 승인되지 않은 행위자가 실수로 액세스할 수 있습니다.
    • 트리거된 오류 메시지에 따라 민감한 정보를 출력할 수 있는 일반 오류 로그입니다.
  • 외부에서 생성된 로그:
    • 외부 구성요소 때문에 민감한 정보가 포함된 로그가 출력됩니다.

Android Log.* 문은 공통 메모리 버퍼인 logcat에 씁니다. Android 4.1(API 수준 16)부터 READ_LOGS 권한을 선언하면 권한이 있는 시스템 앱에만 logcat 읽기 액세스 권한을 부여할 수 있습니다. 하지만, Android에서 지원하는 기기는 매우 다양하며 그중 미리 로드된 애플리케이션이 READ_LOGS 권한을 선언하는 경우가 있습니다. 따라서, logcat에 직접 로깅하는 것은 데이터가 유출되기 쉬우므로 권장하지 않습니다.

디버그 버전이 아닌 애플리케이션 버전에서 logcat에 기록된 모든 로깅을 정리해야 합니다. 민감한 정보가 될 수 있는 모든 데이터는 삭제하세요. 추가 예방 조치로 R8과 같은 도구를 사용하여 경고 및 오류를 제외한 모든 로그 수준을 삭제합니다. 더 자세한 로그가 필요한 경우 시스템 로그를 사용하는 대신 내부 저장소를 사용하여 자체 로그를 직접 관리하세요.

영향

로그 정보 공개 취약점 클래스의 심각도는 컨텍스트와 민감한 정보 유형에 따라 다를 수 있습니다. 전반적으로 이 취약점 클래스의 영향은 잠재적으로 중요한 정보(예: 개인 식별 정보 및 사용자 인증 정보)의 기밀성을 잃는 것입니다.

완화 조치

일반

설계 및 구현 중에는 일반적인 사전 예방 조치로 최소 권한의 원칙에 따라 신뢰 범위를 지정합니다. 민감한 정보가 신뢰 영역을 넘어가거나 신뢰 영역 밖에 있지 않는 것이 이상적입니다. 이렇게 하면 권한을 분리할 수 있습니다.

민감한 정보는 로깅하면 안 됩니다. 가능하면 항상 컴파일 시간 상수만 로깅합니다. 컴파일 시간 상수 주석에 ErrorProne 도구를 사용할 수 있습니다.

트리거된 오류에 따라 민감한 정보를 비롯하여 예상치 못한 정보를 포함할 수 있는 문을 출력하는 로그를 사용하지 않습니다. 로그와 오류 로그에 출력되는 데이터는 최대한 예측 가능한 정보만 포함해야 합니다.

logcat에 로깅하지 않습니다. logcat에 로깅을 하면 READ_LOGS 권한을 사용하는 앱으로 인해 개인 정보 보호 문제가 발생할 수 있기 때문입니다. 또한 알림을 트리거하거나 로깅을 쿼리할 수 없기 때문에 비효율적입니다. 애플리케이션이 개발자 빌드 전용 logcat 백엔드를 구성하는 것이 좋습니다.

대부분의 로그 관리 라이브러리를 사용하면 로그 수준을 정의할 수 있으므로 디버그 로그와 프로덕션 로그 간에 서로 다른 양의 정보를 로깅할 수 있습니다. 제품 테스트가 완료되는 즉시 '디버그'가 아닌 것으로 로그 수준을 변경합니다.

프로덕션에서는 가능한 한 많은 로그 수준을 삭제하세요. 프로덕션에 로그를 유지해야 한다면 로그 구문에서 상수가 아닌 변수를 삭제합니다. 다음과 같은 시나리오가 발생할 수 있습니다.

  • 프로덕션에서 모든 로그를 삭제할 수 있습니다.
  • 프로덕션에서 경고 로그 및 오류 로그를 유지해야 합니다.

두 경우 모두 R8과 같은 라이브러리를 사용하여 로그를 자동 삭제합니다. 로그를 수동으로 삭제하려고 하면 오류가 발생하기 쉽습니다. 코드 최적화의 일환으로 R8을 설정하여 디버깅을 위해 유지하되 프로덕션에서는 제거하려는 로그 수준을 안전하게 삭제할 수 있습니다.

프로덕션에 로깅하려는 경우 문제 발생 시 조건부로 로깅을 종료하는 데 사용할 수 있는 플래그를 준비합니다. 문제 대응용 플래그는 배포 안전성, 배포 속도 및 용이성, 로그 수정의 완벽성, 메모리 사용량, 모든 로그 메시지를 스캔하는 데 드는 성능 비용 등에 우선순위를 두어야 합니다.

R8을 사용하여 프로덕션 빌드에서 Logcat 로그 제거

Android 스튜디오 3.4 또는 Android Gradle 플러그인 3.4.0 이상에서 R8은 코드 최적화 및 축소를 위한 기본 컴파일러입니다. 하지만 R8을 사용 설정해야 합니다.

R8이 ProGuard를 대체했지만 프로젝트의 루트 폴더에 있는 규칙 파일은 여전히 proguard-rules.pro라고 합니다. 다음 스니펫은 프로덕션에서 경고와 오류를 제외한 모든 로그를 삭제하는 샘플 proguard-rules.pro 파일을 보여줍니다.

-assumenosideeffects class android.util.Log {
    private static final String TAG = "MyTAG";
    public static boolean isLoggable(java.lang.String, int);
    public static int v(TAG, "My log as verbose");
    public static int d(TAG, "My log as debug");
    public static int i(TAG, "My log as information");
}

다음 샘플 파일 proguard-rules.pro는 프로덕션에서 모든 로그를 삭제합니다.

-assumenosideeffects class android.util.Log {
    private static final String TAG = "MyTAG";
    public static boolean isLoggable(java.lang.String, int);
    public static int v(TAG, "My log as verbose");
    public static int d(TAG, "My log as debug");
    public static int i(TAG, "My log as information");
    public static int w(TAG, "My log as warning");
    public static int e(TAG, "My log as error");
}

R8은 앱 축소 기능과 로그 제거 기능을 제공합니다. R8을 로그 제거 기능에만 사용하려면 proguard-rules.pro 파일에 다음을 추가합니다.

-dontwarn **
-dontusemixedcaseclassnames
-dontskipnonpubliclibraryclasses
-dontpreverify
-verbose

-optimizations !code/simplification/arithmetic,!code/allocation/variable
-keep class **
-keepclassmembers class *{*;}
-keepattributes *

프로덕션에서 민감한 정보가 포함된 최종 로그 정리

민감한 정보 유출을 방지하려면 디버그 버전이 아닌 애플리케이션 버전에서 logcat에 로깅하는 모든 로그를 정리합니다. 민감한 정보가 될 수 있는 모든 데이터는 삭제하세요.

예:

Kotlin

data class Credential<T>(val data: String) {
  /** Returns a redacted value to avoid accidental inclusion in logs. */
  override fun toString() = "Credential XX"
}

fun checkNoMatches(list: List<Any>) {
    if (!list.isEmpty()) {
          Log.e(TAG, "Expected empty list, but was %s", list)
    }
}

Java

public class Credential<T> {
  private T t;
  /** Returns a redacted value to avoid accidental inclusion in logs. */
  public String toString(){
         return "Credential XX";
  }
}

private void checkNoMatches(List<E> list) {
   if (!list.isEmpty()) {
          Log.e(TAG, "Expected empty list, but was %s", list);
   }
}

로그에서 민감한 정보 수정

민감한 정보를 로그에 포함해야 하는 경우 로그를 출력하기 전에 정리하여 민감한 정보를 삭제하거나 난독화하는 것이 좋습니다. 이렇게 하려면 다음 기법 중 하나를 사용합니다.

  • 토큰화. 토큰을 통해 보안 비밀을 참조할 수 있는 암호화 관리 시스템과 같은 보관소에 민감한 정보가 저장되어 있다면 민감한 정보 대신 토큰을 로깅합니다.
  • 데이터 마스킹. 데이터 마스킹은 되돌릴 수 없는 단방향 프로세스입니다. 이 방식은 구조상 원본과 비슷해 보이는 민감한 정보 버전을 생성하지만, 필드 내에 포함된 가장 민감한 정보는 숨깁니다. 예: 신용카드 번호 1234-5678-9012-3456XXXX-XXXX-XXXX-1313으로 대체합니다. 앱을 프로덕션으로 출시하기 전에 보안 검토 절차를 완료하여 데이터 마스킹의 사용을 철저하게 검토하는 것이 좋습니다. 경고: 민감한 정보를 일부분만 공개해도 보안에 큰 영향을 미칠 수 있는 경우(예: 비밀번호 처리) 데이터 마스킹을 사용하면 안 됩니다.
  • 수정. 수정은 마스킹과 비슷하지만 필드 내에 포함된 모든 정보를 숨깁니다. 예: 신용카드 번호 1234-5678-9012-3456XXXX-XXXX-XXXX-XXXX로 대체합니다.
  • 필터링. 아직 형식 문자열이 존재하지 않는 경우, 원하는 로깅 라이브러리에 형식 문자열을 구현하여 로그 구문에서 상수가 아닌 값을 쉽게 수정할 수 있도록 합니다.

로그 출력은 다음 코드 스니펫에서와 같이 모든 로그가 출력되기 전에 정리되도록 하는 '로그 새니타이저' 구성요소를 통해서만 실행해야 합니다.

Kotlin

data class ToMask<T>(private val data: T) {
  // Prevents accidental logging when an error is encountered.
  override fun toString() = "XX"

  // Makes it more difficult for developers to invoke sensitive data
  // and facilitates sensitive data usage tracking.
  fun getDataToMask(): T = data
}

data class Person(
  val email: ToMask<String>,
  val username: String
)

fun main() {
    val person = Person(
        ToMask("name@gmail.com"),
        "myname"
    )
    println(person)
    println(person.email.getDataToMask())
}

Java

public class ToMask<T> {
  // Prevents accidental logging when an error is encountered.
  public String toString(){
         return "XX";
  }

  // Makes it more difficult for developers to invoke sensitive data
  // and facilitates sensitive data usage tracking.
  public T  getDataToMask() {
    return this;
  }
}

public class Person {
  private ToMask<String> email;
  private String username;

  public Person(ToMask<String> email, String username) {
    this.email = email;
    this.username = username;
  }
}

public static void main(String[] args) {
    Person person = new Person(
        ToMask("name@gmail.com"),
        "myname"
    );
    System.out.println(person);
    System.out.println(person.email.getDataToMask());
}